代码的执行结果是什么呢?
public static void main(String[] args) {
int i = 1;
i = i++;
int j = i++;
int k = i + ++i * i++;
System.out.println("i = " + i);
System.out.println("j = " + j);
System.out.println("k = " + k);
}
1)i = i++; 字节码解析
会先执行 = 号右边的,将 i 压入栈中,操作数栈为 1,然后 i 自增,局部变量表中 i 的值会从 1 变成 2,等号右边操作完成,然后是赋值操作,将操作数栈结果赋值 i 变量,则 i 变量的值为 1。
2)int j = i++; 字节码解析
首先执行等号右边操作,将 i 压入操作数栈中值为 1,然后 i 进行自增操作,局部变量表 i 的值就为 2,执行赋值操作,将操作数栈的 1 赋值给 j ,则局部变量表中 j 的值就为 1。
3)int k = i + ++i * i++; 字节码解析
首先执行等号右边的操作,将 i 压入到操作数栈中值为 2,然后先执行乘法操作,++i 操作是先自增,在局部变量表中,i 的值就为 3,然后将 i 压入到数据栈中值为 3 ,i++ 操作将局部变量表中 i 的值压入到操作数栈中值为3,然后执行自增操作,变量表中 i 的值就为 4,从数据栈中 pop 出 2 个最后压入栈的值进行乘法操作 3 * 3 = 9,然后 push 到栈中,此时栈中的值有两个,一个的最开始压入栈的 2 和 刚刚 push 栈的 9,从栈中取出两个数执行加法操作,结果为 11 ,然后压入栈中赋值给变量表中的 k,此时 k 的值为 11。
执行结果:
i = 4
j = 1
k = 11
总结:
1)赋值 =,最后计算
2)= 右边的从左到右加载值依次压入操作数栈
3)实际先算哪个,看运算符优先级
4)自增、自减操作都是直接修改变量的值,不经过操作数栈
5)最后赋值之前,临时结果也是存储在操作数栈
1)某个类只能有一个实例(构造器私有化)
2)它必须自行创建实例( 含有一个该类的静态变量来保存这个唯一的实例)
3)它必须自行向整个系统提供这个实例(对外提供获取该类实例对象的方式直接暴露,用静态变量声明的方法获取)
常见单例有三种饿汉式和三种懒汉式共六种。
1)直接实例化饿汉式(简洁直观)
/**
* 直接实例化饿汉式
*/
public class Code_02_Singleton1 {
/**
* 1、构造器私有化
* 2、自行创建,并且用静态变量保存
* 3、向外提供实例
* 4、强调这是一个单例,我们可以用final修改
*/
public static final Code_02_Singleton1 INSTANCE = new Code_02_Singleton1();
private Code_02_Singleton1() {
}
}
2) 静态代码块饿汉式(适合复杂实例化)
/**
* 静态代码块饿汉式(适合复杂实例化)
*/
public class Code_02_Singleton2 {
public static final Code_02_Singleton2 INSTANCE;
static {
INSTANCE = new Code_02_Singleton2();
}
private Code_02_Singleton2() {
}
}
3)枚举式 (最简洁)
/**
* 枚举式 (最简洁)
*/
public enum Code_02_Singleton3 {
/**
* 枚举类型:表示该类型是有限的几个
*/
INSTANCE
}
1)线程不安全(适用于单线程)
/**
* 线程不安全(使用于单线程)
*/
public class Code_02_Singleton4 {
/**
* 1、构造器私有化
* 2、用一个静态变量保存这个唯一的实例
* 3、提供一个静态方法,获取这个实例对象
*/
public static Code_02_Singleton4 instance;
private Code_02_Singleton4() {
}
public static Code_02_Singleton4 getInstance() {
if(instance == null) {
instance = new Code_02_Singleton4();
}
return instance;
}
}
2)双重检查(线程安全,适用于多线程)
/**
* 双重检查(线程安全,适用于多线程)
*/
public class Code_02_Singleton5 {
// 加 volatile 作用:防止指令重排, 当实例变量有修改时,能刷到主存中去是一个原子操作,并且保证可见性。
public static volatile Code_02_Singleton5 instance;
private Code_02_Singleton5() {
}
public static Code_02_Singleton5 getInstance() {
if(instance == null) {
synchronized (Code_02_Singleton5.class) {
if(instance == null) {
instance = new Code_02_Singleton5();
}
}
}
return instance;
}
}
3)静态内部类模式 (适用于多线程)
/**
* 静态内部类模式 (适用于多线程)
*/
public class Code_02_Singleton6 {
/**
* 1、内部类被加载和初始化时,才创建INSTANCE实例对象
* 2、静态内部类不会自动创建, 不会随着外部类的加载初始化而初始化,他是要单独去加载和实例化的
* 3、因为是在内部类加载和初始化时,创建的,因此线程安全
*/
public static class Inner {
private static final Code_02_Singleton6 INSTANCE = new Code_02_Singleton6();
}
private Code_02_Singleton6() {
}
public static Code_02_Singleton6 getInstance() {
return Inner.INSTANCE;
}
}
总结:
1、如果是饿汉式,枚举形式最简单
2、如果是懒汉式,静态内部类形式最简单
1)类初始化过程
2)实例初始化过程
3)方法的重写
1)一个类要创建实例需要先加载并初始化该类
2)一个子类要初始化需要先初始化父类
3)一个类初始化就是执行
1)实例初始化就是执行
1)那些方法不可以被重写
2)对象的多态性
结果如下:
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
代码如下:
/**
* 父类初始化
* 1、j = method()
* 2、父类的静态代码块
*
* 父类实例化方法:
* 1、super()(最前)
* 2、i = test() (9)
* 3、子类的非静态代码块 (3)
* 4、子类的无参构造(最后)(2)
*
*
* 非静态方法前面其实有一个默认的对象this
* this在构造器或 他表示的是正在创建的对象,因为咱们这里是正在创建Son对象,所以
* test()执行的就是子类重写的代码(面向对象多态)
*
* 这里i=test() 执行的就是子类重写的test()方法
*/
public class Code_03_Father {
private int i = test();
private static int j = method();
static {
System.out.print("(1)");
}
public Code_03_Father() {
System.out.print("(2)");
}
{
System.out.print("(3)");
}
public int test() {
System.out.print("(4)");
return 1;
}
public static int method() {
System.out.print("(5)");
return 1;
}
}
/**
* 子类的初始化
* 1、j = method()
* 2、子类的静态代码块
*
* 先初始化父类 (5)(1)
* 后初始化子类 (10) (6)
*
* 子类实例化方法:
* 1、super()(最前)
* 2、i = test() (9)
* 3、子类的非静态代码块 (8)
* 4、子类的无参构造(最后) (7)
*/
public class Code_03_Son extends Code_03_Father{
private int i = test();
private static int j = method();
static {
System.out.print("(6)");
}
public Code_03_Son() {
System.out.print("(7)");
}
{
System.out.print("(8)");
}
public int test() {
System.out.print("(9)");
return 1;
}
public static int method() {
System.out.print("(10)");
return 1;
}
public static void main(String[] args) {
Code_03_Son s1 = new Code_03_Son(); // 5 1 10 6 9 3 2 9 8 7
System.out.println();
Code_03_Son s2 = new Code_03_Son(); // 9 3 2 9 8 7
}
}
1)方法的参数传递机制
2)String、包装类等对象的不可变性
1)形参是基本数据类型
2)形参是引用数据类型
代码如下:
/**
* 方法的参数传递机制
*/
public class Code_04_ParameterPassing {
public static void main(String[] args) {
int i = 1;
String str = "hello";
Integer num = 200;
int[] arr = {
1, 2, 3, 4, 5};
MyData myData = new MyData();
change(i, str, num, arr, myData);
System.out.println("i = " + i); // 1
System.out.println("str = " + str); // hello
System.out.println("num = " + num); // 200
System.out.println("arr = " + Arrays.toString(arr)); // 2, 2, 3, 4, 5
System.out.println("my.a = " + myData.a); // 11
}
public static void change(int j, String s, Integer n, int[] a, MyData m) {
j += 1;
s += "world";
n += 1;
a[0] += 1;
m.a += 1;
}
}
class MyData {
int a = 10;
}
结果如下:
i = 1
str = hello
num = 200
arr = [2, 2, 3, 4, 5]
my.a = 11
首先看一道编程题如下:
有 n 步台阶,一次只能上 1 步或者 2 步,共有多少种走法?
分析如图,当 n 等于 1 或者 2 时,走法就等于 n,从第三层台阶开始,每一层台阶为前两层台阶走法之和。
用 one、two 这两个变量来存储 n 的最后走一步和最后走两步,从第三层开始走,用 sum 来保存前两次的走法的次数,sum = two + one; 然后 two 移到 one,one 移到 sum 循环迭代。
代码如下:
/**
* 编程题:有 n 步台阶,一次只能上 1 步或者 2 步,共有多少种走法
*/
public class Code_05_StepProblem {
@Test
public void test() {
// 时间复杂度 ...
// long start = System.currentTimeMillis();
// System.out.println(recursion(40)); // 165580141
// long end = System.currentTimeMillis(); // 537
// System.out.println(end - start);
// 时间复杂度 O(n)
long start = System.currentTimeMillis();
System.out.println(iteration(40)); // 165580141
long end = System.currentTimeMillis(); // 0
System.out.println(end - start);
}
// 递归实现
public int recursion(int n) {
if(n < 1) {
return 0;
}
if(n == 1 || n == 2) {
return n;
}
return recursion(n - 2) + recursion( n - 1);
}
// 迭代实现
public int iteration(int n) {
if(n < 1) {
return 0;
}
if(n == 1 || n == 2) {
return n;
}
int two = 1; // 一层台阶,有 1 走法, n 的前两层台阶的走法
int one = 2; // 二层台阶,有 2 走法, n 的前一层台阶的走法
int sum = 0; // 记录一共有多少中走法
for(int i = 3; i <= n; i++) {
sum = two + one;
two = one;
one = sum;
}
return sum;
}
}
总结:
1)方法调用自身称为递归,利用变量的原值推出新值称为迭代。
2)递归
优点:大问题转化为小问题,可以减少代码量,同时代码精简,可读性好;
缺点:递归调用浪费了空间,而且递归太深容易造成堆栈的溢出。
3)迭代
优点:代码运行效率好,因为时间复杂度为 O(n),而且没有额为空间的开销;
缺点:代码不如递归简洁。
1)就近原则
2)变量的分类
3)非静态代码块的执行:每次创建实例对象都会执行
4)方法的调用规则:调用一次执行一次
1)声明的位置
2)修饰符
3)值存储的位置
4)作用域
5)生命周期
1)局部变量与实例变量重名
在实例变量前面加 “this.”
2)局部变量与类变量重名
在类变量前面加 “类名.”
代码如下:
/**
* 成员变量和局部变量
*/
public class Code_06_LocalAndMemberVariable {
public static int s;
int i;
int j;
{
int i = 1;
i++;
j++;
s++;
}
public void test(int j) {
j++;
i++;
s++;
}
public static void main(String[] args) {
Code_06_LocalAndMemberVariable obj1 = new Code_06_LocalAndMemberVariable();
Code_06_LocalAndMemberVariable obj2 = new Code_06_LocalAndMemberVariable();
obj1.test(10);
obj1.test(20);
obj2.test(30);
System.out.println(obj1.i + "," + obj1.j + "," + obj1.s); // 2 1 5
System.out.println(obj2.i + "," + obj2.j + "," + obj2.s); // 1 1 5
}
}
在 Spring 的配置文件中,给 bean 加上 scope 属性来指定 bean 的作用域如下:
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
支持当前事务的情况:
不支持当前事务的情况:
其他情况:
修改项目中web.xml文件
<filter>
<filter-name>CharacterEncodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>UTF-8param-value>
init-param>
<init-param>
<param-name>forceEncodingparam-name>
<param-value>trueparam-value>
init-param>
filter>
<filter-mapping>
<filter-name>CharacterEncodingFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
修改tomcat中server.xml文件
<Connector URIEncoding="UTF-8" port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
流程说明(重要):
1、客户端(浏览器)发送请求,直接请求到 DispatcherServlet。
2、DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。
3、解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
4、HandlerAdapter 会根据 Handler 来调用真正的处理器来处理请求,并处理相应的业务逻辑。
5、处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View。
6、ViewResolver 会根据逻辑 View 查找实际的 View。
7、DispaterServlet 把返回的 Model 传给 View(视图渲染)。
8、把 View 返回给请求者(浏览器)
解决方案有三种如下:
1、写 SQL 语句的时候 写别名
2、在MyBatis的全局配置文件中开启驼峰命名规则,前提是符合驼峰命名规则
<setting name="mapUnderscoreToCameLCase" value="true" />
3、在Mapper映射文件中使用 resultMap 自定义映射
<resultMap type="com.atguigu.pojo.Employee" id="myMap">
<id cloumn="id" property="id"/>
<result column="last_name" property="lastName" />
<result column="email" property="email" />
<result column="salary" property="salary" />
<result column="dept_id" property="deptId" />
resultMap>
常用基本命令 - 进程类,centos 6 和 centos 7 及以上命令有些不同
service 服务名 start
service 服务名 stop
service 服务名 restart
service 服务名 reload
service 服务名 status
#查看服务的方法 /etc/init.d/ 服务名
#通过 chkconfig 命令设置自启动
#查看服务 chkconfig -list l grep XXX
chkconfig -level 5 服务名 on/off
Linux 系统有 7 种运行级别 (runlevel) : 常用的是级别 3 和 5 。
systemctl start 服务名(xxx.service)
systemct restart 服务名(xxxx.service)
systemctl stop 服务名(xxxx.service)
systemctl reload 服务名(xxxx.service)
systemctl status 服务名(xxxx.service)
#查看服务的方法 /usr/lib/systemd/system
#查看服务的命令
systemctl list-unit-files
systemctl --type service
#通过systemctl命令设置自启动
自启动 systemctl enable service_name
不自启动 systemctl disable service_name
1、创建分支
git branch <分支名>
git branch -v 查看分支
2、切换分支
git checkout <分支名>
一步完成: git checkout -b <分支名>
3、合并分支
先切换到主干 git checkout master
git merge <分支名>
4、删除分支
先切换到主干 git checkout master
git branch -D <分支名>
5、工作流介绍
简单来说就是 master 分支上线,如果 master 出问题,会创建一个 hotfix 分支进行解决 bug ,解决完后合并到 master 分支和 develop 分支,保持一个同步,有新的分支开发完成就会和 develop 分支合并,然后创建一个 release 分支进行测试,完成后在合并到 master 和 develp ,保持一致。
Redis 提供了两种不同形式的持久化的方式。
1)什么是 RDB 呢?
指定时间间隔从内存中的数据集快照写入磁盘,也就是行话讲的 Snapshot 快照,它的恢复是将快照文件读取到内存中。
2)RDB 备份是如何执行的?
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
3)什么是 fork ?
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”,一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
4)RDB 保存的文件
在 redis.conf 的配置文件中,默认保存文件的名称叫 dump.rdb
rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下
5)RDB 保存的策略
15 分钟 1 次添加 key 的操作,5 分钟 10 次添加 key 的操作,1 分钟 10000 次添加 key 的操作都会触发保存策略。
6)RDB 的备份
先通过 config get dir 查询 rdb文件的目录
将 *.rdb 的文件拷贝到别的地方
7)RDB 的恢复
关闭 Redis
先把备份文件拷贝到拷贝到工作目录下
启动 Redis,备份数据会直接加载
8)RDB 的优点
9)RDB 的缺点
虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
在备份周期在一定间隔时间做一次备份, 所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
1)什么是 AOF 呢?
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
2)AOF 默认不开启,需要手动在配置文件中配置
3)可以在redis.conf中配置文件名称,默认为 appendonly.aof
4)AOF 和 RDB 同时开启,redis 听谁的
系统默认取AOF的数据。
5)AOF 文件故障备份
AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。
6)AOF 文件故障恢复
如遇到AOF文件损坏,可通过
redis-check-aof --fix appendonly.aof 进行恢复
7)AOF 同步频率设置
8)Rewrite
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof。
9)Redis如何实现重写?
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
10)何时重写?
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
11)AOF 优点
12)AOF 缺点
1)主键自动建立唯 一 索引
2)频繁作为查询条件的字段应该创建索引
3)查询中与其它表关联的字段,外键关系建立索引
4)频繁更新的字段不适合创建索引,因为每次更新不单是更新了记录还会更新索引
5)单键组索引的选择问题,who? 在高并发下领向创建组合索引
6)意询中排序的字段,排序字段若通过索引法访问将大大提高排序速度
7)查询中统计或者分组字段
1)表记录太少
2)经常增删改的表
因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件数据重复且分布平均的表字段,因此应该只为最经常查询和最经常排序的数据列建立索引。
3)注意,如果某个数据列包含许多重复的内容,为它建立索弓|就没有太大的实际效果。
参考如下文章:
jvm虚拟机(二)自动垃圾回收机制(GC)
JVM垃圾回收机制
数据类型 | 使用场景 |
---|---|
String | 比如:我想知道什么时候封锁一个IP(某一个IP地址在某一段时间内访问的特别频繁,那有可能这个IP可能存在风险,所以对它进行封锁),使用Incrby命令记录当前IP访问次数 存储用户信息【id,name,age】 存储方式:set(userKey,用户信息字符串) |
Hash | 存储用户信息【id,name,age】 存储方式:Hset(key,filed,value) hset(userKey,id,101) hset(userKey,name,“admin”) hset(userKey,age,23) 这样存储的好处:当修改用户信息某一项属性值时,直接通过filed取值,并修改值 -----修改案例------ hget(userKey,id) hset(userKey,id,102) ------------------------------ 为什么不使用String存储? 获取方式:get(userKey) 会把参数为userKey对应的用户信息字符串全部进行反序列号,而用户信息字符串包含了用户所有的信息 如果我只修改用户的ID,那反序列化的其他信息其实是没有任何意义的 序列化与反序列化是由IO进行操作的,使用String类型存储增加了IO的使用次数,降低了程序的性能 对值为某类信息时不建议使用String类型存储 |
List | 实现最新消息的排行,还可以利用List的push命令,将任务存在list集合中,同时使用另一个pop命令将任务从集合中取出 Redis——list数据类型来模拟消息队列。 比如:电商中的秒杀就可以采用这种方式来完成一个秒杀活动 |
set | 特殊之处:可以自动排重。 比如微博中将每个人的好友存在集合(Set)中 如果求两个人的共同好友的操作,我们只需要求交集即可。(交/并集命令) |
Zset | 有序集合(sorted set),以某一个条件为权重,进行排序。 比如:京东看商品详情时,都会有一个综合排名,还有可以安装价格、销量进行排名 |
1)背景
他们都是基于 Lucene 搜索服务器基础上开发,一款优秀的,高性能的企业级搜索服务器,【是因为他们都是基于分词技术构建的倒排索引的方式进行查询】
2)开发语言
Java
3)诞生时间
Solr:2004年诞生
ES:2010年诞生
4)主要区别
单点登录: 一处登录多处使用!
前提:单点登录多使用在分布式系统中
一处登录,处处运行
Demo:
参观动物园流程
检票员=认证中心模块
1、我直接带着大家进动物园,则会被检票员拦住【看我们是否有票】,没有【售票处买票】
登录=买票
2、我去买票【带着票,带着大家一起准备进入动物园】 检票员check【有票】
Token = piao
3、我们手中有票就可以任意观赏动物园的每处景点
京东:单点登录,是将 token 放入到 cookie 中
案例:将浏览器的 cookie 禁用,则在登录京东则失效,无论如何登录不了
背景: 在分布式系统中如何处理高并发的
由于在高并发的环境下,来不及同步处理用户发送的请求,则会导致请求发生阻塞,比如说,大量的 insert,update 之类的请求同时到达数据库 MySQL, 直接导致无数的行锁表锁,甚至会导致请求堆积过多,从而触发 too many connections ( 链接数太多 ) 错误,使用消息队列可以解决 【异步通信】
1)异步
2)并行
3)排队
4)消息队列在电商中的使用场景
5)消息队列的弊端
消息的不确定性: 延迟队列 和 轮询技术来解决问题即可!
学完尚硅谷面试题第一季,感觉讲的比较基础,第二季见。