因为某些原因从上家公司离职,于是有机会跟着面试官疯狂学习;在面试中发现自己的技术也有很大的问题,就算是一个查漏补缺的机会吧!就这次的面试经历我总结整理了一份我面试被问到的问题以及我认为重要的知识点,分享出来做个记录!
继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
多态:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。
Java有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法);也就是方法重写(子类继承父类并重写父类中已有的或抽象的方法)和对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
同一个对象,在不同时刻体现出来的不同状态
多态的关键是每个子类都要重写方法,实现了继承了同样的方法名称但是又有每个的特点,就像龙生九子,每个不一样,有两个好处,一个是类关系清晰,另一个是方法调用方便,只要有传入实参就行。
多态是Java面向对象三个特征中的一个也是做主要的一个,所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
多态的实现
Java实现多态有三个必要条件:继承、重写、向上转型。
只有满足了这三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
向上转型
父类对象通过子类对象去实例化,实际上就是对象的向上转型。向上转型是不需要进行强制类型转换的,但是向上转型会丢失精度。
向下转型
所谓向下转型,也就是说父类的对象可以转换为子类对象,但是需要注意的是,这时则必须要进行强制的类型转换。
多态的好处
由下图可知JDK包含JRE在其中也有JVM,我认为在回答这个问题也可以说说JVM;
JDK(Java Development Kit)Java开发工具包也是核心,只有安装了JDK配好环境变量才可以运行成功;主要包含三部分:
JRE(Java Runtime Environment)Java运行时环境,他会运行在JVM上;在JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,lib就是JVM工作所需要的类库的.class文件已经是Jar包了;
JVM(Java Virtual Machine)Java虚拟机转换环境,正是因为有JVM所以Java的跨平台性比较好(只要在Java虚拟机上运行的目标代码也就是字节码那么只需编译一次就可以可以在多种平台上不加修改地运行);我们编译的.Class文件就是在JVM上运行的文件,它在解释class的时候需要调用解释所需要的类库lib(jre包含lib类库);
一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主类是指包含main()方法的类。而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子类。应用程序
的主类不一定要求是public类,但小程序的主类要求必须是public类。主类是Java程序执行的入口点。
Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同
大小的内存空间。
Java中的数据类型分为两种,基本数据类型和引用数据类型;
基本数据类型
引用数据类型
存储方式:引用类型在堆里而基本类型在栈里。栈空间小且连续,存取速度比较快;在堆中则需要new,对基本数据类型来说空间浪费率太高;
传值方式:基本类型是在方法中定义的非全局基本数据类型变量,调用方法时作为参数是按数值传递的;引用数据类型变量,调用方法时作为参数是按引用传递的;
Java提供了8种包装类;基本类型对应的包装类型:
int ----> Integer(多)
char ----> Character(多)
byte ----> Byte
short----> Short
long ----> Long (多)
float----> Float
double—> Double
boolean —>Boolean
1)每个基本类型都存在一个默认的包装类类型(引用类型);
2)Object可统一所有数据,包装类的默认值是null;
3)将基本数据类型封装成对象的好处在于可以在对象中定义更多的功能方法操作该数据;
4)常用于基本数据类型与字符串之间的转换;
5)包装类实际上就是持有了一个基本类型的属性,作为数据的存储空间(Byte中有一个byte属性),还提供了常用的转型方法,以及常量,即可以存储值,又具备了一系列的转型方法和常用常量,比直接使用基本类型的功能更强大;
自动装箱:将基本类型用它们对应的引用类型包装起来,调用valueOf(int i)(int---->Integer)
自动拆箱:将包装类型转换为基本数据类型,调用intValue()(Integer—>int)
我这里根据功能来列举如果问到肯定多多益善么!
判断功能
// (常用)比较两个字符串内容是否相同,区分大小写;
public boolean equals (Object anObject);
//比较两个字符串的内容是否相同,不区分大小写;
public boolean equalsIgnoreCase (String anotherString);
//判断对象是否为空;
public boolean isEmpty();
//判断字符串是否包含在指定字符串中;
public boolean contains(String s);
//判断字符串是否以指定的字符串开头
public boolean startsWith(String prefix);
//判断字符串是否以指定的字符串结束
public boolean endsWith(String suffix);
获取功能
//返回此字符串的长度(实际长度);
public int length ();
//将指定的字符串连接到该字符串的末尾(拼接的功能);
public String concat (String str);
//返回指定索引处的char值;
public char charAt (int index);
//返回指定子字符串第一次出现在该字符串内的索引;
public int indexOf (String str);
//返回一个子字符串,从beginIndex开始截取字符串到字符串结尾;
public String substring (int beginIndex);
//返回一个子字符串,从beginIndex到endIndex截取字符串。含beginIndex,不含endIndex;
public String substring (int beginIndex, int endIndex);
转换功能
//转换小写
String toLowerCase();
//转换大写
String toUpperCase();
//将字符串转换成字符数组
public char[] toCharArray();
//将int类型转换成字符串
public static String valueOf(int i);
//使用平台默认编码集(GBK)将字符串转换成字节数组
public byte[] getBytes();
分割功能
//将字符串按照regex拆分为字符串数组
public String[] split(String regex);
/**
* 案例
*/
String str = "hji-hi-sd-bih";
String[] strArray = str.split("-");//遇到“——”进行分割
for(int k = 0;k < strArray.length;k++){
System.out.println(strArray[k]);
}
替换功能
//将新的字符串替换掉以前的旧的子串;
public String replace(String oldStr,String newStr):
//去除两端空格
public String trim()
String:字符串是常量,作为方法形参传递,不会改变实际参数,一旦被赋值不能被更改;每次对String的操作都会生成新的String对象,这样效率低下并且会浪费有限的内存空间;
StringBuffer:线程安全的可变字符序列,能够被多次的修改并且不产生新的未使用对象,执行效率低(字符串缓冲区);
StringBuilder:线程不安全的类,能够被多次的修改并且不产生新的未使用对象,单线程程序中使用,不同步,执行效率高;
将对象封装到StringBuilder中,调用reverse方法反转。
public static void main(String[] args) {
//键盘录入字符串,将字符串进行反转------>结果String
Scanner sc = new Scanner(System.in) ;
//提示并录入数据
System.out.println("输入一个字符串:");
String line = sc.next() ;
StringBuilder sb = new StringBuilder(line) ;
String result = sb.reverse().toString() ;
System.out.println("result:"+result);
}
可变、线程安全、效率低
添加功能
//将这些数据添加到指定的缓冲区末尾,返回值是StringBuffer本身
StringBuffer append(int/boolan/long…String/StringBuffer…);
//在指定的位置处插入指定str字符串
public StringBuffer insert(int offset,String str);
public static void main(String[] args) {
//创建一个缓冲区对象
StringBuffer sb = new StringBuffer() ; //底层char [] char = new char[16] ;
System.out.println("sb:"+sb); //空的
sb.append("hello");
sb.append("A");
sb.append(true);
sb.append(12);
System.out.println("sb:"+sb);//helloAtrue12
StringBuffer sb1 = new StringBuffer();
sb1.append("hello").append("Java").append(100);
System.out.println("sb1:"+sb1);//helloJava100
//insert指定字符插入
sb.insert(3, "kaka") ;
System.out.println("sb:"+sb);//helkakaloAtrue12
}
删除功能
//删除指定位置处的字符,返回字符串缓冲区本身(重点)
StringBuffer deleteCharAt(int index)
//删除从指定位置到指定位置结束(end-1),返回字符串缓冲区本身
public StringBuffer delete(int start,int end)
public static void main(String[] args) {
//创建缓冲区对象
StringBuffer sb = new StringBuffer() ;
sb.append("hello").append("Java").append("100") ;
System.out.println(sb);//helloJava100
System.out.println("sb:"+sb.deleteCharAt(5));// helloava100
//第二个索引开始删除5-1个字符
System.out.println("sb:"+sb.delete(2, 5));//heava100
}
反转功能
//将缓存区中的字符序列—反转
public StringBuffer reverse()
//将字符串缓冲区对象—转成String
public String toString()
public static void main(String[] args) {
//键盘录入字符串,将字符串进行反转------>结果String
Scanner sc = new Scanner(System.in) ;
//提示并录入数据
System.out.println("输入一个字符串:");
String line = sc.next() ;
StringBuffer sb = new StringBuffer(line) ;
String result = sb.reverse().toString() ;
System.out.println("result:"+result);
}
替换功能
//使用指定的字符串替换,从指定位置开始,到指定位置结束(end-1);
public StringBuffer replace(int start,int end,String str)
public static void main(String[] args) {
//创建一个缓冲区对象
StringBuffer sb = new StringBuffer() ;
//追加内容
sb.append("hello").append("java").append("EE") ;
System.out.println("sb:"+sb);//hellojavaEE
//从第5个位置开始到7个位置结束进行替换
System.out.println("replace:" + sb.replace(5, 7, "kaka"));//hellokakavaEE
}
截取功能
//start开始直到结束
public String substring(int start)
//start开始,end-1处结束进行截取
public String substring(int start,int end)
可变、线程不安全、效率高
//创建Stringbuilder对象
StringBuilder strB = new StringBuilder();
//append(String str)/append(Char c):字符串连接
System.out.println(strB.append("ch").append("111").append('c'));//ch111c
//toString():返回String类的对象
System.out.println(strB.toString());//ch111c
//setCharAt(int i, char c):将第 i 个代码单元设置为 c(可以理解为替换)
strB.setCharAt(2, 'd');
System.out.println(strB);//chd11c
//insert(int offset, String str)/insert(int offset, Char c):在指定位置之前插入字符(串)
System.out.println(strB.insert(2, "LS"));//chLSd11c
System.out.println(strB.insert(2, 'L'));//chLLSd11c
//delete(int startIndex,int endIndex):删除起始位置(含)到结尾位置(不含)之间的字符串
System.out.println(strB.delete(2, 4));//chSd11c
String---->StringBuffer
public static void main(String[] args) {
String s = "hello" ;
//方式1:通过StringBuffer的构造方法
StringBuffer sb = new StringBuffer(s) ;
System.out.println(sb);//hello,类型还是StringBuffer
//方式2:空参构造+append(...)
StringBuffer sb2 = new StringBuffer() ;
sb2.append(s) ;
System.out.println(sb2);//hello
}
StringBuffer----->String
public static void main(String[] args) {
StringBuffer buffer = new StringBuffer("world") ;
//public String toString() 利用toString
String str2 = buffer.toString() ;
System.out.println(str2);
}
String----->StringBuilder
public static void main(String[] args){
String s = "hello";
StringBuilder sb = new StringBuilder(s);
System.out.println(sb);
}
StringBuilder----->String
public static void main(String[] args){
StringBuilder sb = new StringBuilder();
sb.append("abc").append("efg");
String s = sb.toString();
System.out.println(s);
}
方法的重载(Overload)和重写(Override)都是实现多态的方式,前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分;
重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写;父类的静态方法能够被子类继承,但是不能被子类重写,即使子类的静态方法与父类中的静态方法完全一样,也是两个不同的方法;
抽象类和普通类的区别
包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,和普通类一样,同样可以拥有成员变量和普通的成员方法,抽象类和普通类的区别如下:
抽象类和接口区别
四种访问修饰符分别是:public /protected/default(默认)/private
String类是final类,不可以被继承。
final修饰符:可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、
修饰变量表示该变量是一个常量不能被重新赋值;
finally代码块中:一般作用在try-catch代码块中,在处理异常时通常将一定要执行的代码方法放在finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
注:有些情况不会执行finally
- 只有与finally对应的try语句块得到执行的情况下,finally语句块才会执行。如果在执行try语句块之前已经返回或抛出异常,那么try对应的finally语句并没有执行;
- 我们在try语句块中执行了System.exit (0) 语句,终止了Java虚拟机的运行;
- 如果在try-catch-finally语句中执行return语句,finally语句在该代码中一定会执行,因为finally用法特殊会撤销之前的return语句,继续执行最后的finally块中的代码;
finalize一个方法:属于所有类的父类Object类的一个方法,也就是说每一个对象都有这么个方法;Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作;调用super.finalize();
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
主要是SpringBoot的启动类上的核心注解SpringBootApplication注解主配置类,在这个注解里面有好几个注解:
有了这个EnableAutoConfiguration的话就会:
打包用命令或者放到容器中运行
打成jar包,使用java -jar xxx.jar运行
打成war包,放到tomcat里面运行
直接用maven插件运行 maven spring-boot:run
直接执行main方法运行
@Resource和@Autowired都可以作为注入属性的修饰,在接口仅有单一实现类时,两个注解的修饰效果相同,可以互相替换,不影响使用。但是他们也有不同的地方:
@Autowired的装配顺序
@Resource的装配顺序
联系:二者均可以使用浏览器解析,都在内存中形成dom模型;
区别:
场景: xml描述数据;html展示数据;
标签来源: xml自定义标记;html预定义标记;
书写规范:
Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化deserialization是一种将这些字节重建成一个对象的过程;
序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,就使得数据能够被轻松地存储和传输。
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程;
1、序列化是干什么的?
简单说就是为了保存在内存中的各种对象的状态,并且可以把保存的对象状态再读出来。虽然你可以用自己的各种方法来保存Object states, 但是Java给你提供一种应该比你自己好的保存对象状态的机制、那就是序列化。
2、什么情况下需要序列化?
break:跳出总上一层循环,不再执行循环(结束当前的循环体);
continue:跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件);
return:程序返回,不再执行下面的代码(结束当前的方法 直接返回);
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
静态编译:在编译时确定类型,绑定对象
动态编译:运行时确定类型,绑定对象
优点: 运行期类型的判断,动态加载类,提高代码灵活度;
缺点: 性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的Java代码要慢很多;
public class Student {
private int id;
String name;
protected boolean sex;
public float score;
}
public class Get {
//获取反射机制三种方式
public static void main(String[] args) throws ClassNotFoundException {
//方式一(通过建立对象)
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通过路径-相对路径)
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//方式三(通过类名)
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}
Redis支持五种数据类型:String(字符串),hash(哈希),list(集合),set(无序集合)及 zset(sorted set有序集合;
查询中用到的关键词主要包含六个,其中select和from是必须的,其他关键词是可选的;
-- MySQL的编写顺序
SELECT 列名 FROM 表名 WHERE 条件 GROUP BY 分组 HAVING 过滤条件 ORDER BY 排序列 LIMIT 起始行,总条数
这六个关键词的执行顺序与sql语句的书写顺序并不是一样的,而是按照下面的顺序来执行 :
from–where–group by–having–select–order by
from:需要从哪个数据表检索数据
where:过滤表中数据的条件
group by:如何将上面过滤出的数据分组
使用聚合函数进行计算
having:对上面已经分组的数据进行过滤的条件
计算所有表达式
select:查看结果集中的哪个列,或列的计算结果
order by:按照什么样的顺序来查看返回的数据
聚合函数是对一组值进行计算并返回单一的值的函数,它经常与 select 语句中的 group by 子句一同使用。
因为我们公司用的分页是pagehelper组件,我也没有去研究它,后来才知道MySQL中的limit并不单单是限制还可以用来实现分页,那么我就分享一下limit的一些用法吧;
limit子句用于限制查询结果返回的数量。
用法:【select * from tableName limit i,n 】
参数:
- tableName : 数据表;
- i : 查询结果的索引值(默认从0开始);
- n : 查询结果返回的数量;
一个参数
如果只给定一个参数n,表示记录数返回最大的记录行数目;等价与等价于LIMIT 0,n;
-- 检索前5个记录行
SELECT * FROM tableName LIMIT 5;
SELECT * FROM tableName LIMIT 0,5;
两个参数
两个参数,第一个参数表示offset, 第二个参数为记录数。
-- 检索记录行6-15
SELECT * FROM tableName LIMIT 5,10;
基本的分页方式
SELECT … FROM … WHERE … ORDER BY … LIMIT …
-- 当t_id等于123时按照id排序去查询51~60的数据
SELECT * FROM tableName WHERE t_id = 123 ORDER BY id LIMIT 50, 10
其实数据的存储引擎有很多,但是我认为只要答出来两个就可以了,那就是MyISAM(默认)和InnoDB;
数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。
存储引擎主要有: 1. MyISAM、2. InnoDB、3. Memory、4. Archive、5. Federated ;
MyISAM存储引擎
MyISAM是MySQL官方提供默认的存储引擎,其特点是不支持事务、表锁和全文索引,对于一些 OLAP(联机分析处理)系统,操作速度快。适用于一些大量查询的应用,但对于有大量写功能的应用不是很好。甚至你只需要update 一个字段整个表都会被锁起来。而别的进程就算是读操作也不行要等到当前 update 操作完成之后才能继续进行。另外,MyISAM 对于 select count(*)这类操作是超级快的。
主要特点:
- 因为没有提供对数据库事务的支持,也不支持行级锁和外键,所以当 INSERT(插入)或 UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。
- 不支持事务,但是每次查询都是原子的;
- 支持表级锁,即每次操作是对整个表加锁;
- 存储表的总行数;
- 一个 MYISAM 表有三个文件:索引文件、表结构文件、数据文件;
- 采用菲聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性;
InnoDB存储引擎
InnoDB存储引擎支持事务,对于一些小的应用会比MyISAM还慢,但是支持行锁及外键约束,所以在写操作比较多的时候会比较优秀。并且,它支持很多的高级应用,例如:事务。InnoDB底层存储结构为B+树, B树的每个节点对应InnoDB的一个page,page大小是固定的,一般设为16k。其中非叶子节点只有键值,叶子节点包含完成数据。
一个InnoDB引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为 2G),受操作系统文件大小的限制;主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持 B+树结构,文件的大调整;
适用场景:
- 经常更新的表,适合处理多重并发的更新请求;
- 支持事务;
- 可以从灾难中恢复(通过 bin-log 日志等);
- 外键约束。只有他支持外键;
- 支持自动增加列属性auto_increment;
SQL 标准定义的四个隔离级别为
char是一种固定长度的类型,无论储存的数据有多少都会固定长度,如果插入的长度小于定义长度,则可以用空格进行填充。而varchar是一种可变长度的类型,当插入的长度小于定义长度时,插入多长就存多长。
数据库事务transanction正确执行的四个基本要素,ACID:
#开启事务
START TRANSACTION;#SET autoCommit = 0;#方式2 设置自动提交为0 关闭自动提交 | 1 开启自动提交
#1账户扣钱
UPDATE account SET money = money - 1000 WHERE id = 1;
#2账户加钱
UPDATE account SET money = money + 1000 WHERE id = 2;
#执行提交 ---成功
COMMIT;
#执行回滚 ---失败
ROLLBACK;
个人认为这个问题比较广,我第一次的时候就说了:一般数据量大的情况下会给字段加索引来添加一个标识提高查询的效率,感觉不是面试官想要的于是就有了以下的总结:
索引(Index)是帮助 MySQL 高效获取数据的数据结构。常见的查询算法,顺序查找,二分查找,二叉排序树查找,哈希散列法,分块查找,平衡多路搜索树B+树(B-tree)
索引的目的是什么?
索引对数据库系统的负面影响是什么
为数据表建立索引的原则有哪些
什么情况下不宜建立索引
索引建立常见原则
逻辑角度索引的分类
索引分为四类:单列索引(普通索引,唯一索引,主键索引)、组合索引、全文索引、空间索引;
1、单列索引:一个索引只包含单个列,但一个表中可以有多个单列索引。
普通索引(index):MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值,纯粹为了查询数据更快一点;
-- 直接创建:
create index index_name on table_name (column(length));
-- 修改表结构添加索引:
alter table table_name add index index_name on(column(length));
-- 创建表的时候创建索引:
create table table_name(
.....
....
....
index index_name(column(length))
)
-- 删除索引:
drop index index_name on table;
唯一索引(unique):索引列中的值必须是唯一的,但是允许为空值;
-- 直接创建:
create unique index index_name on table_name (column(length));
-- 修改表结构添加索引:
alter table table_name add unique index index_name on(column(length));
-- 创建表的时候创建索引:
create table table_name(
.....
....
....
unique index index_name(column(length))
)
主键索引(primary key):主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值,一般是在创建表的时候指定主键,主键默认就是主键索引;
create table table_name(
........
......
.....
primary key(column)
)
2、组合索引:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循最左前缀集合;
alter table table_name add index index_name(column,column,column..);
3、全文索引:只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索引;全文索引就是在一堆文字中通过其中的某个关键字找到该字段所属的记录行;允许有重复值和空值,主要用来查找文本中的关键字,而不是直接与索引中的值比较,它更像是一个搜索引擎,全文索引需要配合match against操作使用,而不是一般的where语句加like;
4、空间索引:空间索引是对空间数据类型的字段建立的索引,MySQL中的空间数据类型有四种,GEOMETRY、POINT、LINESTRING、POLYGON;在创建空间索引时,使用SPATIAL关键字。要求引擎为MyISAM,创建空间索引的列,必须将其声明为NOT NULL;
物理存储角度索引的分类
聚集索引和非聚集索引
1、聚集索引:聚集索引确定表中数据的物理顺序,一个表中只能包含一个聚集索引,但该索引可以包含多个列,聚集索引对于经常要搜索范围值的列特别有效,使用聚集索引找到包含第一个值的行后,便可以确保包含后续索引值的行在物理相邻;
2、聚集索引中索引的叶节点就是数据节点,而非聚集索引的叶节点仍然是索引节点,只不过有一个指针指向相应的数据块;
数据结构角度索引的分类
hash索引和B+Tree索引
1、hash索引:hash索引基于hash表实现,只有查询条件精确匹配hash索引中的所有列才会用到hash索引,存储引擎会为hash索引中的每一列都计算hash码,hash索引中存储的就是hash码,所以每次读取都会进行两次查询;
2、B+Tree索引:B+Tree索引在MyISAM和InnoDB存储引擎中的实现不同
定义
主键:唯一标识一条记录,不能有重复的,不允许为空;
外键:表的外键是另一表的主键, 外键可以有重复的, 可以是空值;
索引:该字段没有重复值,但可以有一个空值;
作用
主键:用来保证数据完整性;
外键:用来和其他表建立联系用的;
索引:是提高查询排序的速度;
个数
主键:主键只能有一个;
外键:一个表可以有多个外键;
索引:一个表可以有多个唯一索引;
唯一索引不一定比普通索引快, 还可能慢;
外连接
左连接(左外连接):以左表作为基准进行查询,左表数据会全部显示出来,右表如果和左表匹配的数据则显示相应字段的数据,如果不匹配则显示为 null;
右连接(右外连接):以右表作为基准进行查询,右表数据会全部显示出来,左表如果和右表匹配的数据则显示相应字段的数据,如果不匹配则显示为 null;
全连接:先以左表进行左外连接,再以右表进行右外连接;
全连接
先以左表进行左外连接,再以右表进行右外连接
内连接
根据某个条件筛选出符合条件的记录,不符合条件的记录不会出现在结果集中,显示表之间有连接匹配的所有行;
交叉连接
交叉连接又叫笛卡尔积,它是指不使用任何条件,直接将一个表的所有记录和另一个表中的所有记录一一匹配;
一张表里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录,再把 Mysql 重启,再 insert 一条记录,这条记录的ID是18还是15?
如果表的类型是MyISAM,那么是18;因为MyISAM表会把自增主键的最大ID记录到数据文件里,重启MySQL自增主键的最大ID也不会丢失;
如果表的类型是InnoDB,那么是15;因为InnoDB表只是把最大ID记录到内存中,所以重启数据库或者对表进行optimize(优化)操作,都会导致最大的ID丢失;
语法:
SELECT 列名 FROM 表名 1 UNION SELECT 列名 FROM 表名2
SELECT 列名 FROM 表名 1 UNION ALL SELECT 列名 FROM 表名2
#合并 t1 和t2两张表的结果。纵向合并,去除重复的记录
SELECT * FROM t1
UNION
SELECT * FROM t2
#合并结果集,不去除重复记录
SELECT * FROM t1
UNION ALL
SELECT * FROM t2
MySQL本身并没有对单表最大记录数进行限制,这个数值取决于你的操作系统对单个文件的限制本身。业界流传是500万行。超过500万行就要考虑分表分库了。阿里规范中提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表;
对于这个问题我们可以先回答锁的概念,然后再说有几种锁;
数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。
加锁是实现数据库并发控制的一个非常重要的技术。当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁。加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更新操作。基本锁类型:锁包括行级锁和表级锁;
锁包含:
范式是具有最小冗余的表结构
第一范式(列都是不可再分):1NF 是对属性的原子性约束,要求属性具有原子性,不可再分解;如果每列都是不可再分的最小数据单元(也称为最小的原子单元),则满足第一范式(1NF);
第二范式(每个表只描述一件事情):2NF 是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;首先满足第一范式,并且表中非主键列不存在对主键的部分依赖。 第二范式要求每个表只描述一件事情;
第三范式(不存在对非主键列的传递依赖):3NF 是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余;第三范式定义是,满足第二范式,并且表中的列不存在对非主键列的传递依赖。除了主键订单编
号外,顾客姓名依赖于非主键顾客编号;
范式化设计优缺点
优点:可以尽量得减少数据冗余,使得更新快,体积小;
缺点:对于查询需要多个表进行关联,减少写得效率增加读得效率,更难进行索引优化;
反范式化
优点:可以减少表得关联,可以更好得进行索引优化;
缺点:数据冗余以及数据异常,数据得修改需要更多的成本;
SELECT TIMESTAMPDIFF(YEAR, birthday, CURDATE())
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页, 而非物理分页,可以在SQL内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。 分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截 方法内拦截待执行的SQL,然后重写SQL,根据dialect方言,添加对应的物理分页语句和物理分页参数;
分页插件原理:
实现MyBatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的SQL,然后重写SQL;
例如:
select * from student
拦截sql后重写为:
select t.* from (select * from student) t limit 0 , 10
Mybatis 动态sql可以在Xml映射文件内,以标签的形式编写动态 sql;
执行原理是根据表达式的值完成逻辑判断并动态拼接 sql 的功能;
Mybatis提供了 9 种动态 sql 标签:
trim:可实现where/set标签的功能;
有4个属性:
prefix:表示在trim标签包裹的SQL前添加指定内容
suffix:表示在trim标签包裹的SQL末尾添加指定内容
prefixOverrides:表示去掉(覆盖)trim标签包裹的SQL指定首部内容,去掉多个内容写法为and |or(中间空格不能省略)(一般用于if判断时去掉多余的AND |OR)
suffixOverrides:表示去掉(覆盖)trim标签包裹的SQL指定尾部内容(一般用于update语句if判断时去掉多余的逗号)
where:类似于where,只有一个以上的if条件满足的情况下才去插入WHERE关键字;
set:用于解决动态更新语句存在的符号问题;
foreach:对集合进行遍历;
详见:MyBatis中mapper.xml中foreach的使用一文;
if:当参数满足条件才会执行某个条件;
choose:按顺序判断其内部when标签中的test条件是否成立,如果有一个成立,则choose结束;
when:条件用于判断是否成立;
otherwise:如果所有的when条件都不满足时,则执行otherwise中的SQL;
bind:可以从OGNL(对象图导航语言)表达式中创建一个变量并将其绑定到上下文;
Mybatis是⼀个优秀的基于Java的持久层框架,它内部封装了JDBC,使开发者只需要关注sql语句本身,⽽不需要花费精⼒去处理加载驱动、创建连接、创建statement等繁杂的过程。
Mybatis通过xml或注解的⽅式将要执⾏的各种statement配置起来,并通过java对象和statement中sql的动态参数进⾏映射⽣成最终执⾏的sql语句,最后由mybatis框架执⾏sql并将结果映射为java对象并返回;
MyBatis ⽀持定制化 SQL、存储过程以及⾼级映射。MyBatis 避免了⼏乎所有的JDBC 代码和⼿动设置参数以及获取结果集。MyBatis 可以使⽤简单的 XML 或注解来配置和映射原⽣信息,将接⼝和 Java 的 POJO映射成数据库中的记录;
MyBatis的优点
MyBatis的缺点
1、 创建SqlSessionFactory
2、 通过SqlSessionFactory创建SqlSession
3、 通过sqlsession执行数据库操作
4、 调用session.commit()提交事务
5、 调用session.close()关闭会话
MyBatis 的工作原理如下图:
Mybatis中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中进行相同的SQL语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存1024条SQL。二级缓存是指可以跨SqlSession的缓存。是mapper级别的缓存,对于mapper级别的缓存不同的sqlsession是可以共享的;
一级缓存原理(sqlsession级别,默认打开)
基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当 Session flush 或close之后,该Session中的所有Cache就将清空。
第一次发出一个查询sql,sql 查询结果写入sqlsession的一级缓存中,缓存使用的数据结构是一个map(key:MapperID+offset+limit+Sql+所有的入参;value:用户信息)。同一个 sqlsession再次发出相同的sql,就从缓存中取出数据。如果两次中间出现 commit 操作(修改、添加、删除),本 sqlsession中的一级缓存区域全部清空,下次再去缓存中查询不到,所以要从数据库查询,从数据库查询到再写入缓存;
二级缓存原理(mapper级别)
二级缓存的范围是Mapper(Namespace)级别(mapper同一个命名空间),mapper 以命名空间为单位创建缓存数据结构,结构是 map。mybatis的二级缓存是通过CacheExecutor实现的。CacheExecutor其实是 Executor的代理对象。所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存在,不存在则查询数据库。
具体使用需要配置:
缓存数据更新机制
当某一个作用域(一级缓存 Session/二级缓存 Namespaces)的进行了C/U/D操作后,默认该作用域下所有 select 中的缓存将被clear;
mybatis.mapper-locations在SpringBoot配置文件中使用,作用是扫描Mapper接口对应的XML文件,扫描的是resources下的mapper文件夹中所有的xml结尾的文件。
mybatis.mapper-locations =classpath:/mappers/*.xml
大多业务操作可以根据下图来做缓存,包括面试官问我我就是这么回答的,但是可能没答到点子上;
如果下次还问到我,我会这么回答:
首先不一致主要分为三种情况:
- 数据库有数据,缓存没有数据;(在读数据的时候会自动把数据库的数据写到缓存,因此不一致自动消除)
- 数据库有数据,缓存也有数据,数据不相等;(数据库更新了,但是删除缓存失败了)
- 数据库没有数据,缓存有数据;(数据库的数据删了,但是删除缓存的时候失败了)
解决方案:
- 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快;
- 定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载;
- 给所有的缓存一个失效期;
双写或者数据库和缓存更新(比较官方的回答)
一旦设计到双写或者数据库和缓存更新等操作,就很容易出现数据一致性的问题。无论是先写数据库,在删除缓存,还是先删除缓存,在写入数据库,都会出现数据一致性的问题;
写和读在多数情况下都是并发的,不能绝对保证先后顺序,就会很容易出现缓存和数据库数据不一致的情况,我们有以下两种方式:
方案一:采用延时双删策略
基本思路:
在写库前后都进行删除缓存操作,并且设置合理的超时时间;
基本步骤:
public void write(String key,Object data){
redisUtils.del(key);
db.update(data);
Thread.Sleep(100);
redisUtils.del(key);
}
该方案的弊端:集合双删策略+缓存超时策略设置,这样最差的结果就是在超时时间内数据存在不一致,又增加了写请求的耗时;
方案二:一步更新缓存(基于订阅Binlog的同步机制)
基本思路:
mysql Binlog增强订阅消费+消息队列+增量数据更新到redis;
读redis:热数据基本上都在redis;
写mysql:增删改都是操作mysql;
更新redis数据:mysql的数据操作Binlog,来更新redis;
char型变量是用来存储Unicode编码的字符的,Unicode编码字符集中包含了汉字,所以char型变量中当然可以存储汉字啦。如果某个特殊的汉字没有被包含在Unicode编码字符集中,那么这个char型变量中就不能存储这个特殊汉字;
两个对象:一个是静态区的”kak”,一个是用new创建在堆上的对象;
接口可以继承(extends)接口,而且支持多重继承。抽象类可以实现(implements)接口,抽象类可继承具体类也可以继承抽象类;
断言在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制,个人理解跟If一样;一般来说,断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试时开启。为了保证程序的执行效率,在软件发布后断言检查通常是关闭的。断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为true;如果表达式的值为 false,那么系统会报告一个AssertionError。断言的使用如下面的代码所示:
assert(a > 0); // throws an AssertionError if a <= 0
长度的区别
数组:长度是固定的,length属性判断长度(String中也存在length()方法);
集合:长度是可变的,size()方法;
内容的区别
数组:只能存储同一种数据类型 int[] arr = new int[5];
集合:只能存储一种数据类型;
存储数据类型问题
数组:既可以存储基本数据类型,又可以存储引用数据类型;
集合:只能存储引用数据类型;
这个问题是我面试这段时间认为最难回答的一个问题,原因就他问的范围太大了不知道该咋么回答;就我而言遇到这种问题首先给他说个总的,从最大的开始说起,在说一下里面有哪些内容,包括里面的区别;如果你理解到位了也可以说一下底层的实现那么这时候面试官的眼睛就亮了;下面是我的思路可以参考一下:
首先上一张图,在自己脑子里有一个大概的框架:
集合框架他分为单列集合和双列集合,单列集合都继承自Collection而双列集合就是常说的Map;那么我们先来说说单列集合,单列集合分为List和Set都是继承自Collection 接口(这时候我们可以说List跟Set的区别):
List和Set的区别
List
- 一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引;
- 常用的实现类有 ArrayList、LinkedList 和 Vector;
- 和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变;
- List 支持for循环,也就是通过下标来遍历,也可以用迭代器;
- 存储和取出是一致的;
Set
- 一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性;
- Set 接口常用实现类是 HashSet、LinkedHashSet 以及TreeSet;
- 检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变;
- 因为他无序,无法用下标来取得想要的值,所以只能用迭代器;
- 存储和取出不一致(不能保证该顺序恒久不变);
接着我们可以说一下List的三个子集的区别:
ArrayList、LinkedList 与voctor的区别
ArrayList
底层结构是动态数组,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,查找和遍历的效率较高,增删慢;
LinkedList
底层结构是双向链表,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用;
Voctor
底层结构是数组,支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢;属于线程安全的集合,增删慢,查询慢;
说完之后呢,我们可以往下继续说Set的子集,Set里面最常用的就是HashSet和TreeSet,先说一下HashSet的实现原理以及它如何保证数据的不重复性:
HashSet实现原理
HashSet内部是基于HashMap实现的,底层使用Hash表实现,存取速度快;HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet排列无序,不允许重复的值;
HashSet如何保证数据不重复
HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同)而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode方法来获取的,HashSet首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果equls结果为true,HashSet 就视为同一个元素。如果equals为false就不是同一个元素;
接着就可以说一下TreeSet,他的内部是TreeMap而TreeMap的底层是红黑树一种自平衡的二叉树,所以我们也可以说一下红黑树的相关东西;
TreeSet红黑树
TreeSet底层使用二叉树的原理实现的,排列无序,不可重复;排序存储;内部是TreeMap的SortedSet;对新add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置;
Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用;在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序;
Red - Black - Tree 红黑树结构简述
红黑树一种自平衡的二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black;
public static void main(String[] args) {
//创建TreeSet集合对象
TreeSet<Integer> ts = new TreeSet<Integer>();
//add()添加
ts.add(20) ;
ts.add(18) ;
ts.add(23) ;
ts.add(22) ;
ts.add(17) ;
ts.add(24) ;
ts.add(19) ;
ts.add(18) ;
ts.add(24) ;
//遍历集合
for(Integer i : ts) {
System.out.print(i+" ");//17 18 19 20 22 23 24
}
}
遍历原理:拿上面的数字20,18,23,22,17,24,19,18,24来说:
存储元素:
遍历元素:
使用中序遍历(左根右)
这样单列集合就差不多了;如果你能说到这里并且面试官没有打断你,那么恭喜你就应该稳了;但是我们不要断继续说双列集合:
双列集合Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。Map的常用实现类:HashMap、TreeMap、HashTable等等;接着我们就可以说说他们之间的关系以及各自的特点:
HashTable(线程安全)
Hashtable键不可重复,值可以重复;底层是Hash表;Key和Value都不能为Null;很多映射的常用功能与HashMap类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换;
TreeMap(可排序)
TreeMap实现SortedMap接口,**键不可重复,值可以重复;底层是基于红黑树(Red-Black tree)实现;是一个有序的key-value集合;线程非同步的;**能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常;
HashMap(数组+链表+红黑树,线程不安全)
HashMap**键不可重复,值可以重复;底层是Hash表;key和value都可以为null;**根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致;
说完三个基本特点后,我们可以着重说一下HashMap的实现原理,因为我认为他是整个集合框架里比较重要的一个模块:
HashMap的实现原理
HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变;
HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体;
HashMap基于Hash算法实现的
- 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标;
- 存储时,如果出现hash值相同的key,此时有两种情况:
如果key相同,则覆盖原始值;
如果key不同(出现冲突),则将当前的key-value放入链表中;- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值;
- HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。需要注意JDK1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(N)到O(logN);
HashMap在JDK1.8之前和JDK1.8之后中有哪些不同?
在Java中保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突;
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个单向链表。若遇到哈希冲突,则将冲突的值加到链表中即可;
HashMap JDK1.8之后
我们知道之前查找的时候,根据hash值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度为O(N)。相比于之前的版本,为了降低这部分的开销JDK1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,会将链表转换为红黑树,在这些位置进行查找时可以降低时间复杂度,以减少搜索时间由O(N)变为O(logN);
JDK1.8之前与JDK1.8之后比较
说到这里我个人觉得对这个问题已经有一个比较全面的解答了,这时候就可以等面试官问其他问题了;
总之,在需要频繁读取集合中的元素时推荐使用ArrayList,而在插入和删除操作较多时推荐使用LinkedList。LinkedList的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点;
这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合;
ArrayList的优点如下:
ArrayList的缺点如下:
结构特点
List和Set是存储单列数据的集合,Map是存储键和值这样的双列数据的集合;List中存储的数据是有顺序,并且允许重复;Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的;Set中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的hashcode决定,位置是固定的(Set集合根据hashcode来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的);
实现类
List 接口有三个实现类:
Map接口有三个实现类:
Set 接口有两个实现类:
区别
List集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过list.get(i)方法来获取集合中的元素;Map中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;Set集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定的方式排序,例如TreeSet 类,可以按照默认顺序,也可以通过实现 Java.util.Comparator接口来自定义排序方式;
首先我们要知道迭代器Iterator是什么:Iterator是为了方便的处理集合中的元素,Java中出现了一个对象提供了一些方法专门处理集合中的元素;例如删除和获取集合中的元素.该对象就叫做迭代器(Iterator);
Iterator接口源码中的方法:
队列先进先出,栈先进后出;二者遍历数据速度不同;栈只能从头部取数据也就最先放入的需要遍历整个栈最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,保持数据在遍历前的一致性;队列则不同,他基于地址指针进行遍历,而且可以从头或尾部开始遍历,但不能同时遍历,无需开辟临时空间,因为在遍历的过程中不影像数据结构,速度要快的多;
通过键找值
set< K > keySet() {}
- 获取所有键的集合
- 遍历键的集合,获取到每一个键
- 根据键找值
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>();
//添加元素
map.put("卡卡", "小卡") ;
map.put("牛牛", "小牛") ;
map.put("堂堂", "小堂") ;
map.put("涛涛", "小涛") ;
//获取所有的键的集合
Set<String> set = map.keySet() ;
for(String key :set) {
//通过键获取值 V get(Object key)
String value = map.get(key) ;
System.out.println(key+"="+value);
}
}
根据键值对对象找键和值
public Set
> entrySet() Map.Entry
接口中:K getKey():获取键、V getValue():获取值;
- 获取所有键值对对象的集合
- 遍历键值对对象的集合,获取到每一个键值对对象
- 根据键值对对象找键和值
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>() ; //HashMap:哈希表(元素唯一,无序!)
//添加元素
map.put("卡卡", "小卡") ;
map.put("牛牛", "小牛") ;
map.put("堂堂", "小堂") ;
map.put("涛涛", "小涛") ;
//通过map集合获取所有的键值对对象
Set<Map.Entry<String, String>> entrySet = map.entrySet() ;
for(Map.Entry<String, String> entry:entrySet ) {
//通过entry所有的键值对对象
String key = entry.getKey() ;
String value = entry.getValue() ;
System.out.println(key+"="+value);
}
}
通过迭代器遍历
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>() ; //HashMap:哈希表(元素唯一,无序!)
//添加元素
map.put("卡卡", "小卡") ;
map.put("牛牛", "小牛") ;
map.put("堂堂", "小堂") ;
map.put("涛涛", "小涛") ;
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, String> entry = iterator.next();
System.out.println("key="+entry.getKey()+" ; value="+entry.getValue());
}
}
使用lambda表达式
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>() ; //HashMap:哈希表(元素唯一,无序!)
//添加元素
map.put("卡卡", "小卡") ;
map.put("牛牛", "小牛") ;
map.put("堂堂", "小堂") ;
map.put("涛涛", "小涛") ;
map.forEach((k,v)-> System.out.println("key="+k+" ; value="+v));
}
UDP协议:User Datagram Protocol用户数据报协议,将数据源和目的封装成数据包中,发送数据包到接收端(等于发广播);
TCP协议:Transmission Control Protocol传输控制协议,建立连接形成传输数据的通道(等于和陌生人打电话处理事情);
因为对网络编程不是很熟悉,本人参考TCP的三次握手与四次挥手理解及面试题(很全面)大佬的博文;
三次握手
四次挥手
这种比较类型的问题个人感觉只需要回答一方面就行了,如果你对SSM不是很擅长的话,就抓住SpringBoot的优势说,那么SSM就自然而然的否定了;
Spring Boot 主要有如下优点:
.properties和.yml,它们的区别主要是书写格式不同;
.properties
键值对的形式,文件没有层次结构;
app.user.name = javastack
.yml
文件是树状结构的,可以清晰显示配置的层级;
app:
user:
name: javastack
@PropertySource
注解导入配置,除此之外可以yml可以完全代替properties;Redis是单线程的;那么为啥是单线程的呢?
官方解答
Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络宽带。既然单线程容易实现,而且CPU不会成为瓶颈,那么顺理成章的采用单线程的方案;
线上详解
(1)不需要各种锁的性能消耗
Redis的数据结构并不全是key-value形式的,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash中添加或删除一个对象,这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加;总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现的死锁而导致的性能消耗;
(2)单线程多进程集群方案
单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。所以单线程、多进程的集群不失为一个时髦的解决方案;
(3)CPU消耗
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU。但是如果CPU称为Redis的瓶颈,或者不想让服务器其它CPU核闲置,那怎么办?可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系型数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程中就可以了;
小结
优点
缺点
一个字符串类型的值能存储最大能存储512M;
什么是Redis持久化? 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失;
Redis提供两种持久化机制RDB(默认)和AOF机制;
RDB:是Redis DataBase缩写快照
RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期;记录Redis数据库的所有键值对,在某个时间点将数据写入一个临时文件持久化结束后,用这个临时文件替换上次持久化的文件达到数据恢复;
优点:
缺点:
数据安全性低;RDB 是间隔一段时间进行持久化,如果持久化之间Redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候;
AOF:持久化
AOF持久化(即Append Only File持久化),是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。当两种方式同时开启时数据恢复Redis会优先选择AOF恢复;
优点:
缺点:
俩种持久化对比
我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是
指当Redis中缓存的key过期了,Redis如何处理;
一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能。在这种情况下,当 Redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整;
Redis主要消耗的是内存;
如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回);
你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容;
1、缩短键值的长度
2、共享对象池
对象共享池指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存;
3、字符串优化
4、编码优化
5、控制key的数量
Redis穿透就是用户请求透过Redis去请求mysql服务器,导致mysql压力过载。但一个web服务里,极容易出现瓶颈的就是mysql,所以才让Redis去分担mysql 的压力,所以这种问题是万万要避免的;
一般的缓存系统,都是按照key去缓存查询,如果不存在对用的value,就应该去后端系统查找(比如DB数据库)。一些恶意的请求会故意查询不存在的key,请求量很大就会对后端系统造成很大的压力。这就叫做缓存穿透;
解决方法:
Redis雪崩就是Redis服务由于负载过大而宕机,导致mysql的负载过大也宕机,最终整个系统瘫痪;也就是某key对应的数据存在,但在Redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期,一般都会从数据库中加载数据并设置到缓存中,这个时候大并发的请求可能会瞬间把数据库压垮;
解决方法:
缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率;
会话缓存(Session Cache)
最常用的一种使用 Redis 的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗? 幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台Magento 也提供 Redis 的插件;
可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性;
全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进。 再次以Magento为例,Magento提供一个插件来使用 Redis作为全页缓存后端。 此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面;
队列
Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop操作。 如果你快速的在 Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用Redis创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用 Redis 作为 broker,你可以从这里去查看;
排行榜
Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的 10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可: 当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子,用Ruby实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到;
发布/订阅(了解,没用过网上看的)
Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统;
分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现;
计数器
可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量;
对于一些用户请求,如果是查询类操作并无大碍,但其中有些是涉及写入操作的一旦重复了,可能会导致很严重的后果,例如交易的接口如果重复请求可能会重复下单;防止连续点击,主要分为前端限制和接口限制;
前端方面
防抖是控制次数,节流是控制频率;
防抖(debounce)
防抖就是指触发事件后在n秒内函数只能执行一次,如果在n秒内又触发了事件,则会重新计算函数执行时间;当一个动作连续触发,则只执行最后一次;
函数防抖的简单实现:
const _debounce = (func, wait) => { let timer; return () => { clearTimeout(timer); timer = setTimeout(func, wait); }; };
函数防抖在执行目标方法时,会等待一段时间。当又执行相同方法时,若前一个定时任务未执行完,则
clear
掉定时任务,重新定时;
节流(throttle)
节流就是指连续触发事件但是在n秒中只执行一次函数;限制一个函数在一定时间内只能执行一次;
1、函数节流的setTimeout`版简单实现
const _.throttle = (func, wait) => { let timer; return () => { if (timer) { return; } timer = setTimeout(() => { func(); timer = null; }, wait); }; };
- 函数节流的目的,是为了限制函数一段时间内只能执行一次。因此,通过使用定时任务,延时方法执行。在延时的时间内,方法若被触发,则直接退出方法。从而,实现函数一段时间内只执行一次。
2、函数节流的时间戳版简单实现
const throttle = (func, wait) => { let last = 0; return () => { const current_time = +new Date(); if (current_time - last > wait) { func.apply(this, arguments); last = +new Date(); } }; };
- 其实现原理,通过比对上一次执行时间与本次执行时间的时间差与间隔时间的大小关系,来判断是否执行函数。若时间差大于间隔时间,则立刻执行一次函数。并更新上一次执行时间;
加loading
给按钮上添加loading,直到请求完成后才结束loading,可以在一定程度防止重复请求;
接口方面
在数据库添加唯一字段
在数据库建表的时候在ID字段添加主键约束,账号,名称的信息添加唯一性约束。确保数据库只可以添加一条数据;可以有效的防止了数据重复提交;
加一张关系表
每次请求在进行业务处理前都往该表插入一条记录,请求完成后删除该记录。通过使用数据库中的唯一键保证在同一时刻只有一个请求可以正常插入request表,插入失败的请求将不做业务处理。但是如果请求量比较大的话会损耗DB的性能且不容易扩展;
设置请求次数
通过用户具有唯一性的ID验证用户的身份,每次用户调用接口,将此用户的ID进行记录,在有效的时间之内,用户每调用一次接口,给调用的次数+1,当超过指定的次数时(次数根据自己的时间情况进行定义),将调用这个接口的用户ID加入黑名单中,当锁定之后移除在黑名单中的用户ID进行解锁(锁定时间根据实际情况进行定义),但是每次用户在短时间内调用接口,实时记录的ID,次数,时间可以存到数据库或者Redis;
一般当存储占用100G+时、数据增量每天 200w+时、单表条数 1 亿条+时会考虑分库分表;分库分表的目的就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的;
写在前面:因为我接触过的项目数据量都不是很大,所以没有涉及到分库的概念,但是面试是会问到的,之后看到了IT邦德大佬的MySQL分库分表,何时分?怎么分?一文,个人认为写的很到位,所以借鉴这篇文章结合我的理解,整理一下;
回答思路可以是:首先回答什么是分库分表,然后分别说一说他们会在什么场景下使用,然后说一下他们的优缺点就OK了;
什么是分库分表
MySQL是我们用的比较多的数据库,如果在使用过程中出现性能问题,会采用 mysql 的横向扩展,使用主从复制来提高读性能;要是解决写入问题,需要进行分库分表。分库分表是业务发展到一定阶段,数据积累到一定量级而衍生出来的解决方案。当数据量级到达一个阶段, 写入和读取的速度会出现瓶颈,即使是有索引,索引也会变的很大,而且数据库的物理文件大的会使备份和恢复等操作变的很困难。这个时候已经严重危害到了业务,最有效的解决方案就是分库分表了;数据库表的拆分解决的问题主要是存储和性能问题,MySQL在单表数据量达到一定量级后,性能会急剧下降,相比较于收费数据库来说,还是处于弱势,但是表的拆分这个策略却适用于几乎所有的关系型数据库;
分库
分库是指把一个数据库拆分为多个数据库,一般分为垂直分库和水平分库;
垂直分库:
垂直分库以表为依据,按照业务归属不同,将不同的表拆分到不同的业务库中。每个库可以放在不同的服务器上,核心理念是专库专用;
- 结果:垂直分库的结果是每个库的表结构都不一样;每个库的数据也不一样,没有交集;所有库的并集是全量数据;
- 场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块;
- 分析:到这一步,基本上就可以服务化了。例如随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中。再有随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。
水平分库:
水平分库是以字段为依据,按照一定策略(hash、range 等),将一个库中的数据拆分到多个库中;
- 结果:水平分库的结果是每个库的结构都一样,但是每个库的数据都不一样,没有交集;所有库的并集是全量数据;
- 场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库;
- 分析:库多了,IO 和 CPU 的压力自然可以成倍缓解;
分表
分表指的是通过一定规则,将一张表分解成多张不同的表,一般分为垂直分表和水平分表;
垂直分表:
垂直分表即“宽表拆窄表”,以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。垂直分表一般是表中的字段较多,将冗余字段,不常用字段,数据较大,长度较长(例如 text 类型字段)的拆分到“扩展表“。一般是针对那种几百列的宽表,也可以避免在查询时,数据量太大造成的“跨页”问题。
- 结果:垂直分表的结果是每个表的结构都不一样;每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;所有表的并集是全量数据;
- 场景:系统绝对并发量并没有上来表的记录并不多,但是字段多并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈;
- 分析:垂直分表比较适用于那种字段比较多的表,原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读 IO。拆了之后,要想获得全部数据就需要关联两个表来取数据;
通常我们按以下原则进行垂直拆分
- 把不常用的字段单独放在一张表;
- 把 text,context等大字段拆分出来放在附表中;
- 经常组合查询的列放在一张表中;
水平分表:
水平分表是以字段为依据,按照一定策略(hash、range 等),将一个表中的数据拆分到多个表中,也称为库内分表;
- 结果:水平分表的结果是每个表的结构都一样;每个表的数据都不一样,没有交集;所有表的并集是全量数据;
- 场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了 SQL 效率,加重了 CPU 负担,以至于成为瓶颈;
- 分析:表的数据量少了,单次 SQL 执行效率高,自然减轻了 CPU 的负担;
优缺点
垂直拆分优点
- 跟随业务进行分割,和最近流行的微服务概念相似,方便解耦之后的管理及扩展;
- 高并发的场景下,垂直拆分使用多台服务器的 CPU、I/O、内存能提升性能,同时对单机数据库连接数、一些资源限制也得到了提升;
- 能实现冷热数据的分离;
水平拆分的优点
- 水平扩展能无限扩展,不存在某个库某个表过大的情况;
- 能够较好的应对高并发,同时可以将热点数据打散;
- 应用侧的改动较小,不需要根据业务来拆分;
可以在Maven中导入Mybatis依赖进而导入Mybatis
引入依赖
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.15version>
<scope>runtimescope>
dependency>
配置yml文件
引入数据源、Mybatis配置
#配置数据源
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
#MyBatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml #对应mapper映射xml文件所在路径
编写映射文件
我们在配置的相应的路径下创建Mapper接口文件以及其映射文件即可;
&按位与:不管前面的条件是否正确,后面都执行;
&&逻辑与:前面条件正确时才执行后面,不正确时就不执行;&&效率比较高;当且仅当两个操作数均为 true时,其结果才为true,只要有一个为false就为false;
一般创建线程常用继承Thread类和实现Runnable接口两种方式,当然也可以使用ExecutorService、Callable、Future实现有返回结果的多线程;这里主要介绍前两种;
继承Thread类实现多线程
一个类只要继承了Thread就是多线程的实现类,必须重写run方法;
步骤:
自定义一个类,这个类继承自Thread类
在这个类重写Thread类中的run方法(一般存储耗时的代码)
在主线程中开启子线程 main线程
- 创建自定义线程类对象
- 使用线程类对象调用start()方法
public class TestExtendsThreads {
public static void main(String[] args) {
//创建线程对象
MyThread1 t1 = new MyThread1();
t1.start();//线程启动(JVM调用run方法)
for(int i = 1; i<50; i++){
System.out.println("Main:"+i);
}
}
}
//线程类 自定义线程
class MyThread1 extends Thread{
public void run(){
//线程的执行具有随机性,存储"耗时代码"
for(int i = 1; i<50; i++){
System.out.println("MyThread1:"+i);
}
}
}
实现Runnable接口方式实现多线程
只需要实现一个抽象方法:public void run();
步骤:
自定义一个类 , 实现 Runnable接口
实现接口中提供的功能:public abstract void run() ; 比较耗时的操作
用户线程main,创建资源类对象
- 创建Thread类对象,将上面对象作为参数传递
- 分别启动线程
Thread类的构造方法
public Thread(Runnable target):分配一个新的Thread对象
public Thread(Runnable target, String name):分配一个新的Thread对象
Thread.currentThread():获取正在运行的线程
public class TestImplementsRunnable {
public static void main(String[] args) {
//创建资源类对象象
MyRunnable mr1 = new MyRunnable();
//创建线程类对象
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr1);
//分别启动线程
t1.start();
t2.start();
for(int i = 1; i<50; i++){
System.out.println("Main:"+i);
}
}
}
class MyRunnable implements Runnable{
public void run(){
for(int i = 1; i<50; i++){
//Thread.currentThread()正在运行的线程
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
使用实现Callable接口的方式
步骤:
创建线程池对象: ExecutorService pool = Executors.newFiexdThreadPool(int nThreads){}
提交异步任务 submit(Callable call)
- Callable:接口 (Functional Interface:函数式接口:接口中只有一个抽象方法)
- V call():计算结果
关闭资源:shutdown():关闭线程池资源对象
public class MyCallable implements Callable {
@Override
public Object call() throws Exception{
for(int i = 0 ;i < 200; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
return null;
}
}
public static void main(String[] args) {
//创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);
//提交异步任务
pool.submit(new MyCallable());
pool.submit(new MyCallable());
//关闭资源
pool.shutdown();
}
采用实现Runnable、Callable 接口的方式创建多线程
优点:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个 target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模
型,较好地体现了面向对象的思想;
缺点:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法;
使用继承Thread类的方式创建多线程
优点:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用 this 即可获得当前线程;
缺点:
线程类已经继承了Thread类,所以不能再继承其他父类;
Runnable和Callable的区别
并发:多个任务在同一个CPU核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行;
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”;
串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题;
比如:
并发 = 俩个人用一台电脑;
并行 = 俩个人分配了俩台电脑;
串行 = 俩个人排队使用一台电脑;
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务;
多线程的好处
可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务;
多线程的劣势
实现多线程就需要在主线程创建线程对象,但是线程因为需求的不同往往不是理性状态;当线程被创建并启动以后它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换;
线程的状态_基础(理想化状态)
New(初始状态):当程序使用new关键字创建了一个线程之后(Thread t = new MyThread()),该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值;
Ready(就绪状态):初始状态调用**start()**方法就可以启动线程进入就绪状态等待OS选中并分配时间片;处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了start()此线程立即就会执行;
注:就绪状态是进入到运行状态的唯一入口,也就是说线程要想进入运行状态执行,首先必须处于就绪状态中;
Running(运行状态):当被OS选中并且获得了时间片之后就会进入该状态,如果时间片到期就会回到就绪状态,要想继续执行只能等待下一次被OS选中;
Terminated(终止状态):当所有的代码都执行完毕后由主线程或独立线程调用**run()**方法结束后,进入该状态,并释放时间片,处于终止状态的线程具有继续运行的能力;
线程的状态_等待
但是在真正执行过程中往往不会是理想的状态,我们出门坐公交都会有等车的时候,更何况是线程!因此线程也会有等待的状态;
线程状态_阻塞
我们在坐车的时候如果运气不好你不光要等,在路上有可能会有堵车的情况,因此我们的线程也会有阻塞的状态!
根据阻塞产生的原因不同,阻塞状态可以分为三种;
可以;但是如果我们调用了Thread 的run()方法,它的行为就会和普通的方法一样,会在当前线程中执行。为了在新的线程中执行我们的代码,必须使用Thread.start()方法;
sleep方法和wait方法都可以用来放弃CPU一定的时间也就是暂停线程的执行,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器,详细区别:
线程相关的基本方法有wait(强迫一个线程等待),notify(通知一个线程继续执行),notifyAll(所有线程继续执行),sleep(强迫一个线程睡眠N毫秒),join(等待线程终止),yield(线程让步)等等;
获取和设置线程名称
线程的名字一般在启动前设置,两个名字可以重复但是一般不这样做;
设置守护线程
守护线程是运行在后台的一种特殊进程,在程序中只要有一个线程在运行,整个程序不会消失,设置一个守护线程即使程序结束,后台仍然会继续执行;在Java中垃圾回收线程就是特殊的守护线程;(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
final
boolean
isDaemon():判断一个线程是否为守护线程;join()方法
在线程的操作中加入join()方法(类似于插队)让一个线程强制执行,此线程运行期间其他线程必须等待此线程执行完毕后才可以继续执行(进入就绪再次竞争时间片);
sleep()方法
程序中调用sleep方法,可以使得线程暂时休眠;(类似于睡觉,醒来继续竞争)
yied()方法
在线程操作中,调用yied()方法可以使当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片;(类似与礼让)
线程的优先级
Java程序中所有线程在运行之前都会在就绪状态,因此就存在优先级的问题!默认的优先级是5,
优先级都是自定义的常量:
public static final int MAX_PRIORITY 10 :最大优先级
public static final int NORM_PRIORITY 5 :默认优先级
public static final int MIN_PRIORITY 1 :最小优先级
线程停止
当一个线程运行时,另外一个线程可以直接调用interrupt()方法中断其运行状态,也可以是stop()方法;
线程唤醒(notify)
Object类中的notify() 方法,唤醒在此对象监视器上等待的单个线程让其继续运行,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有notifyAll() ,唤醒再次监视器上等待的所有线程;
其他方法
notifyAll()会唤醒所有的线程,notify()只会唤醒一个线程;
如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁;就必须等其他线程调用notify()或者notifyAll():使用notifyall(),可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify()只能唤醒一个,具体唤醒哪一个线程由虚拟机控制;
当new一个Thread,线程进入了新建状态。调用start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。
而直接执行run() 方法,会把run 方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
interrupt:中断线程的状态(睡眠状态),依然可以执行线程;
stop:线程被强迫终止,在启动线程之前,如果调用线程的stop方法,不会执行线程;
很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是主线程需要在子线程结束后再结束,这时候就要用到join()方法;
System.out.println(Thread.currentThread().getName() + "线程运行开始!");
Thread1 son = new Thread1();
son.setName("线程B");
son.join();
System.out.println("这时thread1执行完毕之后才能执行主线程");
设计模式的水太深就说自己最熟悉的、用得最多的回答,不要自己给自己挖坑;因为我Spring方面用的比较多,spring里面的控制反转有用到工厂模式,有时也会写写单例,所以我一般就说这两个,下面这纯属复制混个眼熟;
所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计和体系结构;
设计模式主要分为三类:
共23种设计模式,包括:
Abstract Factory(抽象工厂模式),Builder(建造者模式),Factory Method(工厂方法模式),Prototype(原始模型模式),Singleton(单例模式);Facade(门面模式),Adapter(适配器模式),Bridge(桥梁模式),Composite(合成模式),Decorator(装饰模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(解释器模式),Visitor(访问者模式),Iterator(迭代子模式),Mediator(调停者模式),Memento(备忘录模式),Observer(观察者模式),State(状态 模式 ),Strategy(策略 模式 ),Template Method(模板方法模式),Chain Of Responsibility(责任链模式);
面试被问到关于设计模式时,可以拣常用的回答:
工厂模式(IOC里面用到了):
工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例;
代理模式:
给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为远程代理、虚拟代理、保护代理、Cache 代理、防火墙代理、同步化代理、智能引用代理;
适配器模式:
把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作;
模板方法模式:
提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式(Collections 工具类和 I/O 系统中都使用装潢模式)等;
用过Spring框架就一定都会听过Spring的IOC(控制反转) 、DI(依赖注入)这两个概念,属于Spring的两大核心;
IOC(inverse of control)是面向对象编程中的一种设计原则,用来降低系统之间的耦合度。在程序代码编译之先就已经确立相互间的依赖关系 (由代码控制的依赖关系),将对象的创建由容器完成,事先不知道所对应的对象,只有执行到对应的位置才可以知道对应的对象,也就是说传统的Java开发模式中,当需要一个对象时,我们会自己使用new或者间接调用构造方法创建一个对象。而在spring开发模式中,spring容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用spring提供的对象就可以了,这是控制反转的思想;
DI(dependency injection)依赖注入,如果说控制反转是一种设计思想,而依赖注入是控制反转的一种实现方式;编译的时候不知道,只有在运行时由容器动态注入,就是在spring创建时为对应的属性赋值;spring使用javaBean对象的set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想;
AOP(Aspect Oriented Programming)面向切面编程,与OOP面向对象编程相辅相成;在OOP思想中我们将事物纵向抽成一个个的对象,基本单元是类。而在AOP面向切面编程中,我们将分散在各处的相同业务集中管理的编程方式横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想,基本单元是Aspect(切面);
比如我们把功能分为核心业务功能和周边功能,面向切面的编程思想是在添加周边功能的时候,不影响核心业务功能;假设把用户的请求、新增、删除是核心业务,日志是周边功能,在增加新的日志时不影响核心的业务功能;
AOP底层是动态代理,如果是接口采用JDK动态代理,如果是类采用CGLIB方式实现动态代理;AOP的优势是代码高可用、高复用、后期维护方便;
AOP核心概念
实现AOP的技术,主要分为两大类:静态代理和动态代理
静态代理
指使用AOP框架提供的命令进行编译,从而在编译阶段就可生成AOP代理类,因此也称为编译时增强;优点是可以在不修改目标对象的前提下扩展目标对象的功能;
例子:
1、创建IMath接口
/**
* 接口调用Calculator
* 父类接口指向子类实现
* Created by Kak
*/
public interface IMath {
public int plus(int a ,int b);
public int minus(int a ,int b);
public int multi(int a ,int b);
public int div(int a ,int b);
}
2、创建Calculator实现IMath接口
/**
* Created by Kak
*/
public class Calculator implements IMath{
public int plus(int a, int b){
return a + b;
}
public int minus(int a, int b){
return a - b;
}
public int multi(int a, int b){
return a * b;
}
public int div(int a, int b){
return a / b;
}
}
3、创建StaticProxy.java实现IMath
静态代理类由原始类的接口加辅助功能加原始类的业务方法组成
import java.util.Random;
/**
* Created by Kak
*/
public class StaticProxy implements IMath{
private IMath math;
public StaticProxy(IMath math){
this.math = math;
}
@Override
public int plus(int a, int b) {
long begin = System.currentTimeMillis();
int result = math.plus(a,b);
this.sleeping();
long end = System.currentTimeMillis();
System.out.println("方法耗时:"+(end - begin));
return result;
}
@Override
public int minus(int a, int b) {
long begin = System.currentTimeMillis();
int result = math.minus(a,b);
this.sleeping();
long end = System.currentTimeMillis();
System.out.println("方法耗时:"+(end - begin));
return result;
}
@Override
public int multi(int a, int b) {
long begin = System.currentTimeMillis();
int result = math.multi(a,b);
this.sleeping();
long end = System.currentTimeMillis();
System.out.println("方法耗时:"+(end - begin));
return result;
}
@Override
public int div(int a, int b) {
long begin = System.currentTimeMillis();
int result = math.div(a,b);
this.sleeping();
long end = System.currentTimeMillis();
System.out.println("方法耗时:"+(end - begin));
return result;
}
public void sleeping(){
Random random = new Random(1000);
try {
Thread.sleep(random.nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
优点:
缺点:
如果项目中有多个类,则需要编写多个代理类,工作量大,不好修改,不能应对变化,维护性差;
动态代理
动态代理分为: JDKProxy和CGLIB,默认的策略是如果目标类是接口,则使用 JDK 动态代理技术,否则使用CGLIB来生成代理;
JDK动态接口代理
JDK动态代理就是动态创建代理类,为原始类的对象添加辅助功能,只能代理有接口的实现; 主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。InvocationHandler是一个接口通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编制在一起。Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象;
例子
1、创建IMath接口
2、创建Calculator实现IMath接口
3、创建DynamicProxy加入动态代理实现InvocationHandler接口
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;
/**
* JDK动态代理
* Created by Kak
*/
public class DynamicProxy implements InvocationHandler{
Object targetObject;
public Object getObject(Object SrcObject){
this.targetObject = SrcObject;
//获取代理对象
/**
* 作用:使用动态代理自动给正在执行的对象的接口生成一个新的派生对象,此对象还有invoke方法中附带的逻辑
* 第一个参数:需要执行方法的对象的类加载器
* 第二个参数:需要执行方法的对象实现的接口
* 第三个参数:InvocationHandler的实现对象
*/
Object o = Proxy.newProxyInstance(SrcObject.getClass().getClassLoader(),SrcObject.getClass().getInterfaces(),this);
return o;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long begin = System.currentTimeMillis();
sleeping();
//执行原有的方法
/**
* 第一个参数:方法所在的对象
* 第二个参数:方法执行的实参
*/
Object result = method.invoke(targetObject,args);
long end = System.currentTimeMillis();
System.out.println("方法耗时:" + (end - begin));
return result;
}
public void sleeping(){
try {
Thread.sleep(new Random().nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4、创建测试类
/**
* Created by Kak
*/
public class TestCalculator {
public static void main(String[] args) {
//源对象
Calculator cal = new Calculator();
//自定义的附带有计算耗时的动态代理
DynamicProxy proxy = new DynamicProxy();
IMath newObj = (IMath)proxy.getObject(cal);
int plus = newObj.plus(1, 10);
System.out.println(plus);
}
}
JDK动态代理的优缺点
优势:
劣势:
CGLib 动态代理
CGLIB(Code Generation Library)是一个开源项目,高性能,高质量的代码生成类库,可以在运行期扩展Java类与实现Java接口,动态生成字节码CGLib基于ASM的字节码生成库,可以再运行期动态生成新的class。和JDK动态代理相比较:JDK创建代理有一个限制,就是只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,则可以通过CGLib使用继承的方式实现动态代理;
例子
1、编写Calculator.java需要被代理的类
2、 编写DynamicProxy.java实现cglib代理
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.Random;
/**
* Created by Kak.
*/
public class DynamicProxy implements MethodInterceptor{
Object targetObj;
public Object getObject(Object srcObj){
this.targetObj = srcObj;
//创建代码增强器
Enhancer enhancer = new Enhancer();
//设定增强器的父类
enhancer.setSuperclass(srcObj.getClass());
//设定增强器回调对象
enhancer.setCallback(this);
//获取附加逻辑的新对象
Object o = enhancer.create();
return o;
}
/**
* @param o 需要代理的对象
* @param method 需要执行的方法
* @param objects 执行方法所需要的实参
* @param methodProxy 代理的对象
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
long begin = System.currentTimeMillis();
sleeping();
Object invoke = method.invoke(targetObj, objects);
long end = System.currentTimeMillis();
System.out.println("耗时:"+ (end - begin));
return invoke;
}
public void sleeping(){
try {
Thread.sleep(new Random().nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3、编写测试类
/**
* Created by Kak .
*/
public class TestCglib {
public static void main(String[] args) {
Calculator newObj = (Calculator)new DynamicProxy().getObject(new Calculator());
int minus = newObj.minus(20, 30);
System.out.println(minus);
}
}
注意:
定义通知类可以达到通知的效果;
前置通知(Method Advice):在某些连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常);
后置通知(After Returning Advice):在某连接点正常完成后执行的通知:例如一个方法没有抛出任何异常正常返回; 有异常不执行,无返回值,方法会因为异常而结束;
异常通知(Throws Advice):在方法抛出异常退出时执行的通知;
环绕通知(Around Advice):包围一个链接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。他也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行;
Spring的模型-视图-控制器(MVC)框架是围绕一个DispatcherServlet来设计的,这个Servlet会把请求分发给各个处理器,并支持可配置的处理器映射、视图渲染、本地化、时区与主题渲染等,甚至还能支持文件上传;
DispatcherServlet流程
DispatcherServlet是SpringMVC工作流程的中心,负责调用其他组件,这个类会在系统启动的时候加载;
Http请求到DispatcherServlet
(1) 客户端向服务器发送请求,请求被SpringMVC前端控制器DispatchServlet捕获;
HandlerMapping寻找处理器
(2) DispatcherServle对请求URL进行解析,得到请求资源标识符(URL),然后根据该URL调用HandlerMapping将请求映射到处理器HandlerExcutionChain;
调用处理器Controller
(3) DispatcherServlet将请求提交到 Controller;
Controller调用业务逻辑处理后,返回ModelAndView
(4)(5)调用业务处理和返回结果:Controller调用业务逻辑处理后,返回ModelAndView;
DispatcherServlet查询ModelAndView
(6)(7)处理视图映射并返回模型: DispatcherServlet查询一个或多个ViewResoler视图解析器,找到ModelAndView指定的视图;
ModelAndView反馈浏览器HTTP
(8) Http响应:DispatcherServle通过model解析出ModelAndView()中的参数进行解析最终展现出完整的view并返回给客户端;
其实SpringMVC中的注解在SpringBoot中也必不可少,大部分都不算是独有的!
@RestController注解相当于@ResponseBody + @Controller合在一起的作用;
使用@Controller注解,在对应的方法上,视图解析器可以解析return 的jsp,html页面,并且跳转到相应页面;若返回json等内容到页面,则需要加@ResponseBody注解;
@RestController注解,相当于@Controller+@ResponseBody两个注解的结合,返回json数据不需要在方法前面加@ResponseBody注解了,但使用@RestController这个注解,就不能返回jsp,html页面,视图解析器无法解析jsp,html页面;
HTTP请求的组成:状态行、请求头、消息主体三部分组成;
HTTP响应的组成:状态行、响应头、响应正文;
雪花算法(SnowFlake)算法,是Twitter开源的分布式id生成算法。其核心思想就是:使用64位long类型的数字作为全局唯一ID,且ID引入了时间戳,基本上保持自增的;
组成部分
这64个bit中,其中1个bit是不用的,然后用其中的41bit作为毫秒数,用10bit作为工作机器id,12bit作为序列号;
可以分为四部分
雪花算法的优点
雪花算法的缺点
冒泡排序比较的是相邻的元素,如果第一个数比第二个数大(小),就交换两个元素,没个相邻元素都这么比,直到比较最后一对,此时最后的数应该是最大(小)的;对所有元素重复此操作,就可以得出一个递增(递减)的数组;
需求:键盘录入一个数组(5个不同的整数)通过冒泡排序将数组进行排序并打印
- 0索引和1索引比较,如果0索引大于1索引的值,交换位置否则位置不变;1索引和2索引比较,方法类似
public class BubbleTest {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int[] arr = new int[5];
for(int i = 0; i < 5; i++){
System.out.println("输入第"+(i+1)+"个数字:");
arr[i] = sc.nextInt();
}
System.out.println("排序前:");
printArray(arr);
BubbleShot(arr);
System.out.println("排序后:");
printArray(arr);
}
//冒泡排序
private static void BubbleShot(int[] arr) {
for(int i = 0; i < arr.length-1; i++){
for(int j = 0; j < arr.length-1-i; j++){
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
//遍历数组
private static void printArray(int[] arr) {
System.out.print("[");
for(int i = 0; i < arr.length; i++){
if(i == arr.length-1){
System.out.println(arr[i] + "]");
}else{
System.out.print(arr[i] + "\t");
}
}
}
}
在SQL中增加HAVING子句原因是,WHERE关键字无法与聚合函数一起使用,HAVING子句可以让我们筛选分组后的各组数据;用having去掉不符合条件的组,having子句中的每一个元素必须出现在select列表中(只针对于mysql);
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程),也就是两个线程相互等待对方释放对象锁;多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止;
要想知道二者的区别就先得整明白什么是线程什么是进程?
- 进程:一个在内存中运行的应用程序。 每个正在系统上运行的程序都是一个进程;
- 线程:进程中的一个执行任务(控制单元), 它负责在程序里独立执行;
注:一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据;
进程与线程的区别
线程的安全是以牺牲效率为代价的,所谓线程安全就是多了个加锁、解锁的操作,比如100亿个操作中都要加锁和解锁,线程是安全了,但效率就下降了。而有些软件是以效率为主的,为了提高效率,就少了加锁,解锁的操作,虽然容易出现并发访问问题,但效率却提高了;
这个问题也是一个范围比较广的问题,我们可以从什么实现城池出发,然后说一下他的作用最后说一下线程池的优点;
什么是线程池
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处;
线程池作用
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率;如果一个线程所需要执行的时间非常长的话,就没必要用线程池了(不是不能作长时间操作,而是不宜。本来降低线程创建和销毁,结果你那么久我还不好控制还不如直接创建线程),况且我们还不能控制线程池中线程的开始、挂起、和中止;
线程池有什么优点
在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行,synchronized可以修饰类、方法、变量;
synchronized底层实现原理
Synchronized的底层是通过一个monitor(监视器锁)的对象来完成,每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权 :
synchronized关键字最主要的三种使用方式
总结: synchronized关键字加到static静态方法和synchronized(class)代码块上都是是给Class类上锁。synchronized关键字加到实例方法上是给对象实例上锁;
synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁;
cd :切换目录
cd ../ ;跳到上级目录
cd /opt ;不管现在到那直接跳到指定的opt文件夹中
cd ~ ;切换当前用户的家目录。root用户的家目录就是root目录
pwd :显示当前工作目录的绝对路径
ls :list的缩写,查看当前目录下的所有文件夹
ls -a ;显示所有文件夹,隐藏文件也显示出来
ls -R ;连同子目录一起列出来
ll:list的缩写,查看当前目录下的所有详细信息和文件夹
ll -a ;显示所有文件,隐藏文件也显示出来
ll -R ;连同子目录内容一起列出来
ll -h ;友好展示详情信息,可以看大小
ll -al ;即能显示隐藏文件又能显示详细列表
touch:创建文件
touch test.txt ;创建test.txt文件
touch /opt/java/test.java ;在指定目录创建test.java文件
mkdir:创建目录
mkdir 文件夹名称 ;在此目录创建文件夹
mkdir /opt/java/jdk ;在指定目录创建文件夹
cat :查看文件命令(concatenate:显示或把多个文本文件连接起来)
cat lj.log ;快捷查看文件命令
Ctrl + c ;暂停显示文件
Ctrl + d ;退出查看文件命令
more:分页查看文件命令 (more:更多的意思)
回车:向下n行,需要定义,默认为1行。
空格键:向下滚动一屏或Ctrl+F
B:返回上一层或Ctrl+B
q:退出more
less:分页查看文件命令 (lese:较少的意思)
less -m 显示类似于more命令的百分比
less -N 显示每行的行号。(大写的N)
两参数一起使用如:less -mN 文件名,如此可分页并显示行号
空格键:前下一页或page down
回车:向下一行
b:后退一页 或 page up
q:退出。
d:前进半页
u:后退半页
tail:查看文件命令(看最后多少行)
tail -10 ;文件名 看最后10行
cp:copy单词缩写,复制功能
cp /opt/java/java.log /opt/logs/ ;把java.log 复制到/opt/logs/下
cp /opt/java/java.log /opt/logs/aaa.log ;把java.log 复制到/opt/logs/下并且改名为
aaa.log
cp -r /opt/java /opt/logs ;把文件夹及内容复制到logs文件中
mv:move单词缩写,移动功能,该文件名称功能
mv /opt/java/java.log /opt/mysql/ ;移动文件到mysql目录下
mv java.log mysql.log ;把java.log改名为mysql.log
rm:删除文件或文件夹(remove:移除的意思)
-f或--force 强制删除文件或目录。删除文件不包括文件夹的文件
-r或-R或--recursive 递归处理,将指定目录下的所有文件及子目录一并删除。
-rf 强制删除文件夹及内容
rm 文件名 ;安全删除命令 (yes删除 no取消)
rm -rf 强制删除文件夹及内容
rm -rf * 删除当前目录下的所有内容。
rm -rf /* 删除Linux系统根目录下所有的内容。系统将完蛋。
find:查找指定文件或目录(find:找到的意思)
* 表示0~多个任意字符。
find -name 文件名;按照指定名称查找在当前目录下查找文件
find / -name 文件名按照指定名称全局查找文件
find -name '*文件名' ;任意前缀加上文件名在当前目录下查找文件
find / -name '*文件名*' ;全局进行模糊查询带文件名的文件
vim:改进版文本编辑器
输入”vim 文件名” 打开文件,刚刚时是”一般模式”。
一般模式:可以浏览文件内容,可以进行文本快捷操作。如单行复制,多行复制,单行删除,多行删除,(退出)等。
插入模式:可以编辑文件内容。
底行模式:可以进行强制退出操作,不保存 :q!
可以进行保存并退出操作 :wq
按下”i”或”a”或”o”键,从”一般模式”,进入”插入模式(编辑模式)”。
在编辑模式下按”Esc” 即可到一般模式
在一般模式下按”:”,冒号进入底行模式。
在一般模式下的快捷键
dd ;删除一整行
X ;向前删除 等同于windowns系统中的删除键
x ;向后删除和大写x相反方向
Ctrl + f ;向后看一页
Ctrl + b ;向前看一页
u ;撤销上一步操作
/word ;向下查找word关键字 输入:n查找下一个,N查找上一个(不管是哪个查找都是全局查找 只不过
n的方向相反)
?log ;向上查找log关键字 输入:n查找上一个,N查找下一个
:1,90s/redis/Redis/g ;把1-90行的redis替换为Redis。语法n1,n2s/原关键字/新关键字/g,n1
代表其实行,n2代表结尾行,g是必须要的
:0 ;光标移动到第一行
:$ ;光标移动到最后一行
:300 ;光标移动到300行,输入多少数字移动到多少行
:w ;保存
:w! ;强制保存
:q ;退出
:q! ;强制退出
5dd ;删除后面5行,打一个参数为自己填写
5x ;删除此光标后面5个字符
d1G ;删除此光标之前的所有
d0 ;从光标当前位置删除到此行的第一个位置
yy ;复制
p ;在光标的下面进行粘贴
P ;在光标的上门进行粘贴
yum install -y lrzsz 命令(实现win到Linux文件互相简单上传文件)
#(实际上就是在Linux系统中下载了一个插件)下了了此安装包后就可以实现win系统到linux之间拉文件拉文件
#等待下载完了就可以输入:
rz 从win系统中选择文件上传到Linux系统中
sz 文件名 选择Linux系统的文件复制到win系统中
tar :解压、压缩命令
常用的组合命令:
-z 是否需要用gzip压缩。
-c 建立一个压缩文件的参数指令(create) –压缩
-x 解开一个压缩文件的参数指令(extract) –解压
-v 压缩的过程中显示文件(verbose)
-f 使用档名,在f之后要立即接档中(file)
常用解压参数组合:zxvf
常用压缩参数组合:zcvf
解压命令:
tar -zxvf redis-3.2.8.tar.gz ;解压到当前文件夹
tar -zxvf redis-3.2.8.tar.gz -C /opt/java/ ;解压到指定目录
压缩命令:
tar -zcvf redis-3.2.8.tar.gz redis-3.2.8/ ;
语法 tar -zcvf 压缩后的名称 要压缩的文件
tar -zcvf 压缩后的文件(可指定目录) 要压缩的文件(可指定目录)
ps :process status:进程状态,类似于windows的任务管理器
常用组合:
ps -ef 标准的格式查看系统进程
ps -aux BSD格式查看系统进程
ps -aux|grep redis BSD格式查看进程名称带有redis的系统进程(常用技巧)
//显示进程的一些属性,需要了解(ps aux)
USER //用户名
PID //进程ID号,用来杀死进程的
%CPU //进程占用的CPU的百分比
%MEM //占用内存的的百分比
VSZ //该进程使用的虚拟內存量(KB)
RSS //该进程占用的固定內存量(KB)
STAT //进程的状态
START //该进程被触发启动时间
TIME //该进程实际使用CPU运行的时间
clear:清屏命令
kill 命令用来中止一个进程------------(ps类似于打开任务管理器,kill类似于关闭进程)
kill -5 进程的PID ;推荐,和平关闭进程
kill -9 PID ;不推荐,强制杀死进程
ifconfig:用于查看和更改网络接口的地址和参数,包括IP地址、网络掩码、广播地址,使用权限是超级用户;
如果此命令输入无效,先输入yum -y install net-tools
ifconfig
ping :用于检测与目标的连通性
1、在Windows操作系统中cmdipconfig,查看本机IP地址:
2、再到LInux系统中输入 ping ip地址
按Ctrl + C 可以停止测试
free:显示系统内存
#显示系统内存使用情况,包括物理内存、交互区内存(swap)和内核缓冲区内存。
-b 以Byte显示内存使用情况
-k 以kb为单位显示内存使用情况
-m 以mb为单位显示内存使用情况
-g 以gb为单位显示内存使用情况
-s<间隔秒数> 持续显示内存
-t 显示内存使用总
top:显示当前系统正在执行的进程的相关信息,包括进程 ID、内存占用率、CPU 占用率
-c 显示完整的进程命令
-s 保密模式
-p <进程号> 指定进程显示
-n <次数>循环显示
file:可查看文件类型
file 文件名
重启linux
Linux centos 重启命令:reboot
关机linux
Linux centos 关机命令:halt
同步时间命令
ntpdate ntp1.aliyun.com
查看时间命令
date
Windows适合普通用户进行娱乐办公使用,Linux适合软件开发部署;
Windows是微软开发的操作系统,民用操作系统,可用于娱乐、影音、上网。 Windows操作系统具有强大的日志记录系统和强大的桌面应用。好处是它可以帮我们实现非常多绚丽多彩的效果,可以非常方便去进行娱乐、影音、上网;
Linux的应用相对单纯很多,没有什么绚丽多彩的效果,因此Linux的性能是非常出色的,可以完全针对机器的配置有针对性的优化;
NullPointerException:空指针异常
ClassCastException 类型强制转换异常
IllegalArgumentException 传递非法参数异常
ArithmeticException 算数运算异常
IndexOutOfBoundsException 下标越界异常
NumberFormatException 数字格式异常
ClassNotFindException 加载请求异常
实例化这个概念理解起来比较枯燥,正巧我在网上看到一个例子,分享一下;
你要买一个苹果,售货员给你一个苹果;
你要买一苹果, 相当于 --------- Apple apple = null;
这个时候你并没有拿到苹果---------java没有给你申请内存,这个时候apple还什么内容都没有,只是告诉别人apple是个苹果
售货员给你个苹果 ----------apple = new Apple();
这个时候你拿到了苹果 ---------- java给你开辟了空间,并且apple可以使用苹果的特性,比如:apple.getPrice(),apple.getTaste()
这就是apple的实例化
MyBatis-Plus(简称 MP)是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生;
1、在SpringBoot项目中导入依赖
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.3version>
dependency>
2、编写Mapper接口并继承BaseMapper即可;
3、这样就可以使用Mybatis-Plus的语法了;
如果说实体类的属性和表的列名一一对应,名字一样,那就自动解决了这个问题。但是如果实体类的属性和表的列名不一致,这就需要我们手动的把它们关联起来,我一般在开发中会用一下两种方法:
1、写SELECT语句的时候,顺便写上查出的数据的列名称对应的实体属性;
2、将查出的数据的列名称对应的实体属性单独定义,形式为…
<resultMap id="Result01" type="model.student">
<id column="student_id" property="id" jdbcType="VARCHAR" />
<result column="studnet_name" property="name" jdbcType="VARCHAR" />
<result column="student_sex" property="sex" jdbcType="VARCHAR" />
resultMap>
<select id="studnetInfo" resultMap="Result01">
select * from student
where
student_id=#{id,jdbcType=VARCHAR}
select>
在工作中一般有三种方法
1、使用concat函数进行拼接
#
作为占位符,防止 SQL 注入;
<select id="findList" resultType="com.ninong.admin.Student">
select t1.* from student t1
<where>
<if test="name !=null and name != ''">
and t1.name like concat ('%',#{name},'%')
if>
where>
select>
2、使用bind元素
<select id="findList" resultType="com.ninong.admin.Student">
select t1.* from student t1
<bind name="name" value="'%' + name + '%'" />
<where>
<if test="name !=null">
name LIKE #{name}
if>
where>
select>
3、Java中拼接
//String searchText = "%" + text + "%";
String searchText = new StringBuilder("%").append(text).append("%").toString();
parameterMap.put("name", searchText);
SELECT * FROM tableName WHERE name LIKE #{name};
使用DISTINCT关键字
SELECT DISTINCT <字段名> FROM <表名>
使用Group by关键进行分组;
我之前的一个经理告诉我,评定一个程序员是否优秀首先看的不是他会多少东西,知识面有多广而是看他的写的代码是否规范,是否简洁可读,换句话就是说有没有一个很好的编码习惯。我们可以参考阿里的编码规范《阿里巴巴 Java 开发手册 》来要求自己;
由于构造器不能继承,所以就不能被重写。但是,在同一个类中,构造器是可以被重载的;
jdk1.8Stream流
可以,所有整型包装类对象之间值的比较,全部使用equals方法比较;
面向过程
面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现;
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素;
缺点:没有面向对象易维护、易复用、易扩展;
面向对象
面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装方便我们使用就是面向对象了;
面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了;
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护;
缺点:性能比面向过程低;
可以,接口继承是为了在不修改接口的情况下,扩展接口的功能Java中的类是单继承,但接口可以多继承。比如List接口继承Collection接口;
可以查阅API我举几个常用的例子;
私有方法是可以反射的,那么下一个问题就是:私有(private)属性及方法可以通过反射访问,那么private的意义是什么?
首先我们就得知道什么是反射
在运行过程中,对于任何一个类。都能够知道这个类的所有属性和方法,对于任何一个对象,都能够调用它的任何一个方法和属性这种动态获取的信息以及动态调用对象的方法的功能成为Java的反射机制;
然后我们得知道私有的目的
在一个类中,为了不让外界访问到某些属性和方法,通常将其设置为private,用正常的方式(对象名.属性名,对象名.方法名)将无法访问此属性与方法,但是通过反射机制可以知到这个类中的所有属性和方法。
那么反射机制的存在跟private访问修饰矛盾吗?
然后回答一下这个问题
首先private的存在并不是为了实现Java程序的完全安全,Java的安全机制是层层相扣、十分复杂的。private只是为了Java的正常开发过程中的一种约束,也是符合OOP(面向对象编程)的封装,实现内部的部分不可见。就好比超市仓库会挂一个牌子“闲人免进”,但是如果非要进去的话也不是不行。而且在java开发过程中有时候确实会需要用的反射机制的功能,比如测试/性能等场景下;
我们可以使用分页插件PaginationInnerInterceptor;官方介绍:https://baomidou.com/pages/97710a/#paginationinnerinterceptor
1、config.java中添加以下代码
@Configuration
@MapperScan("com.baomidou.cloud.service.*.mapper*")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2、分页函数
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
* @param queryWrapper 实体对象封装操作类(可以为null),用于作为查询条件,为null时默认查询所有记录。
*/
<E extends IPage<T>> E selectPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
3、测试
@Test
public void testPage(){
//设置分页的条件,从第2的开始查询5条记录
Page<Account> page = new Page<>(2,5);
//分页,默认查询所有的记录
Page<Account> accountPage = accountDao.selectPage(page, null);
accountPage.getRecords().forEach(System.out::println);
}
这两个都属于Mybatis-Plus中的写法;
写法区别
LambdaQueryWrapper
List<CoalPriceExecute> coalPriceExecutes = baseMapper.selectList(new LambdaQueryWrapper<CoalPriceExecute>()
.eq(CoalPriceExecute::getCoalEnterId, dto.getCoalEnterId())
.eq(CoalPriceExecute::getExecuteTime, dto.getExecuteTimeDate())
.ne(CoalPriceExecute::getExecuteStatus, DicConstantEnum.COAL_PRICE_EXECUTE_YZF.getCode()));
QueryWrapper
List<CoalPriceExecute> coalPriceExecutes = baseMapper.selectList(new QueryWrapper<CoalPriceExecute>()
.lambda()
.eq(CoalPriceExecute::getCoalEnterId, dto.getCoalEnterId())
.eq(CoalPriceExecute::getExecuteTime, dto.getExecuteTimeDate())
.ne(CoalPriceExecute::getId, dto.getId())
.ne(CoalPriceExecute::getExecuteStatus, DicConstantEnum.COAL_PRICE_EXECUTE_YZF.getCode()));
使用区别
优势
LambdaQueryWrapper的写法如果有错,则在编译期就会报错,而QueryWrapper需要运行的时候调用该方法才会报错;
这个问题又是一个比较广的一个问题,我们可以先说说什么是事务?然后再说说Redis事务的概念,再说一下Redis事务的三个阶段,最后说一下Redis事务相关命令那么我认为就算完整了;
什么是事务?
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断;事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行;
Redis事务的概念
Redis事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中;
Redis事务的三个阶段
事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队;
Redis事务相关命令
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH四个原语实现的;Redis会将一个事务中的所有命令序列化,然后按顺序执行;
WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令;
MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行;
EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值null;
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出;
UNWATCH命令可以取消watch对所有key的监控;
其它问题
1、Redis事务支持隔离性吗?
Redis是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完
所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的;
2、Redis事务保证原子性吗,支持回滚吗?
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失
败,其余的命令仍会被执行;
Nginx配置文件是nginx.conf
worker_processes 1; # worker进程的数量
events { #事件区块开始
worker_connections 1024; # 每个worker进程支持的最大连接数
} #区块链的结束
http { #http区块开始
include mime.types; # Nginx支持的媒体类型库文件
default_type application/octet-stream; # 默认的媒体类型
server_tokens off; #隐藏版本号
sendfile off; #关闭高效传输模式
keepalive_timeout 65; #连接超时
server { # 第一个Server区块开始,表示一个独立的虚拟
#主机站点
listen 80 ; # 提供服务的端口,默认80
server_name 192.168.31.155 127.0.0.1 ; # 提供服务的域名主机名
charset utf-8; #编码格式
location / { # 第一个location区块开始
root html; # 站点的根目录,相当于Nginx的安装目录
index index.html index.htm; # 默认的首页文件,多个用空格分开
} # 第一个location区块结束
error_page 500502503504 /50x.html; # 出现对应的http状态码时,使用50x.html回应客户
location = /50x.html { # location区块开始,访问50x.html
root html; # 指定对应的站点目录为html
}
}
}
相同点:
Mybatis 的好处:
Hibernate:
二分查找又称折半查找,它是一种效率较高的查找方法;
二分查找要求(前提):
- 必须采用顺序存储结构
- 必须按关键字大小有序排列
二分查找法原理就是将数组分为三部分,依次是中值(所谓的中值就是数组中间位置的值)前,中值,中值后;将要查找的值和数组的中值进行比较,若小于中值 则在中值前面找,若大于中值则在中值后面找,等于中值时直接返回。然后依次 是一个递归过程,将前半部分或者后半部分继续分解为三部分;
单例就是该类只能返回一个实例;
单例所具备的特点
单例实现的主要步骤
饿汉式
先初始化对象,Single类⼀进内存,就已经创建好了对象;
public class Single{
//直接创建对象
private static Single s=new Single();
//私有化构造函数
private Single(){}
//返回对象实例
public static Single getInstance()
{
return s;
}
}
懒汉式
对象是⽅法被调⽤时才初始化,也叫做对象的延时加载;
- Single类进内存,对象还没存在,只有调⽤了getInstance⽅法时,才建⽴对象
public class Single{
//声明变量
private static Single s=null;
//私有构造函数
private Single(){}
//提供对外方法
public static synchronize Single getInstance()
{
if(s==null){
s=new single();
}
return s;
}
}
数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。
解决:在 SqlMapConfig.xml 中配置数据链接池,使用连接池管理数据库链接。
Sql 语句写在代码中造成代码不易维护,实际应用 sql 变化的可能较大,sql 变动需要改变 java 代码。
解决:将 Sql 语句配置在 XXXXmapper.xml 文件中与 java 代码分离。
向 sql 语句传参数麻烦,因为 sql 语句的 where 条件不一定,可能多也可能少,占位符需要和参数一一对应。
解决: Mybatis 自动将 java 对象映射至 sql 语句。
对结果集解析麻烦,sql 变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成 pojo 对象解 析比较方便。
解决:Mybatis自动将sql执行结果映射至java对象;
在项目中jar包的资源越多,管理就越麻烦:
- 繁琐:为每个项目手动导入所需要的jar,前提是需要手机jar包;
- 复杂:如果jar升级,就需要重新搜集jar;
- 冗余:相同的jar包会在不同的项目中保存多份;
Maven是基于项目对象模型(POM),可以通过一小段描述信息来管理项目的构建、报告和文档的软件项目管理工具,同时也是一个项目管理和构建自动化工具,maven能处理从创建、编译、测试、运行、清理、打包、部署 各个环节的项目管理,它还提供了一个仓库的概念,统一的帮助管理项目中的jar包避免冲突问题;最大可能的避免环境配置不同所产生的问题(在你的电脑上能运行,在我的电脑上就不能运行)
Maven中找依赖的顺序:
- 本地仓库
- 私服(没有配置忽略)
- 公共仓库(没有配置忽略)
- 中央仓库
mvn -v:查看版本
mvn clean:对项目进行清理,清理的过程中会删除删除target目录下编译的内容
mvn compile:编译项目源代码
mvn test:对项目的运行测试。
mvn packet:可以打包后的文件存放到项目的target 目录下,打包好的文件通常都是编译后生成的class文件。
mvn install:在本地仓库生成仓库的安装包可以供其他项目引用,同时打包后的文件存放到项目的 target 目录下,生成jar或war
对项目打包有三种打包方式,pom打包,jar包和war包。打包方式在pom.xml文件中进行指定。pom工程一般是聚合工程,代表父工程,负责管理jar包的版本、maven插件的版本等,主要做统一的依赖管理。
jar包就是普通的打包方式,可以是pom工程的子工程。
war包的都是web工程,是可以直接放到tomcat下运行的工程。
打成pom包和jar包的工程在新建的时候可以不需要制定maven项目的原型,达成war包的项目需要制定maven项目原型,指定的原型通常为maven-archetype-webapp,代表web项目。
mvn deploy :发布命令,将打包的文件发布到远程参考,提供其他人员进行下载依赖
Socket(套接字:可以用来实现不同虚拟机或计算机之间的通信)是网络中的一个通讯节点;任意一个Socket都由IP地址+端口号,
Socket分为两种类型:
Socket原理
开发步骤
建立通信连接(会话):
- 创建ServerSocket,监听某个端口,指定端口号;
- 调用accept等待客户端接入;
客户端请求服务器:
- 创建Socket,指定服务器IP+端口号;
- 使用输出流发送请求数据给服务器;
- 使用输入流接收响应数据到客户端(等待)
服务器响应客户端:
- 服务端接收到客户端的请求后,创建新线程并启动;
- 使用输入流接收请求数据到服务器(等待)(关闭)
- 使用输出流发送响应数据给客户端(关闭)
声命周期共三个阶段:
public class Server {
public static void main(String[] args)throws Exception {
//1.创建服务套接字
ServerSocket server = new ServerSocket(6666);
System.out.println("服务器已启动");
//2.调用accept等待客户端
Socket client = server.accept();
//3.通过客户端获取输入输出流
InputStream is = client.getInputStream();
InputStreamReader isr = new InputStreamReader(is,"UTF-8");
BufferedReader br = new BufferedReader(isr);
OutputStream os = client.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os,"UTF-8");
PrintWriter pw = new PrintWriter(osw);
//4.读取数据
String message = br.readLine();
System.out.println("客户端说:"+message);
//5.响应数据
pw.println("我看你好像张永超!");
pw.flush();
//6.关闭
pw.close();
br.close();
client.close();
server.close();
}
}
public class Client {
public static void main(String[] args)throws Exception {
//1.创建客户端,连接指定的IP+端口号
Socket client = new Socket("192.168.1.3",6666);
//2.获取输入输出流
InputStream is = client.getInputStream();
InputStreamReader isr = new InputStreamReader(is,"UTF-8");
BufferedReader br = new BufferedReader(isr);
OutputStream os = client.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os,"UTF-8");
PrintWriter pw = new PrintWriter(osw);
//3.发送数据
pw.println("服务端!");
pw.flush();
//4.接收响应数据
String message = br.readLine();
System.out.println("服务端说:"+message);
//5.关闭
pw.close();
br.close();
client.close();
}
}
客户端----服务端(发送)
服务端----线程处理类(收到)
线程处理(处理)
线程处理类-----客户端(响应)
一、框架机制
二、拦截机制
Struts2 是类级别的拦截, 一个类对应一个request上下文,SpringMVC是方法级别的拦截,一个方法对应一个request上下文,而方法同时又跟一个url对应,所以说从架构本身上SpringMVC就容易实现restful url,而struts2 的架构实现起来要费劲,因为 Struts2 中 Action 的一个方法可以对应一个 url,而其类属性却被所有方法共享,这也 就无法用注解或其他方式标识其所属方法了;
三、性能方面
SpringMVC实现了零配置,由于SpringMVC基于方法的拦截,有加载一次单例模式bean注入。而Struts2是类级别的拦截,每次请求对应实例一个新的Action,需要加载所有的属性值注入,所以,SpringMVC开发效率和性能高于Struts2;
四、拦截机制
Struts2有自己的拦截Interceptor机制,SpringMVC这是用的是独立的Aop方式,这样导致Struts2的配置文件量还是比SpringMVC大;
五、配置方面
spring MVC和Spring是无缝的。从这个项目的管理和安全上也比Struts2高(当然Struts2也可以通过不同的目录结构和相关配置做到SpringMVC一样的效果,但是需要xml配置的地方不少)。SpringMVC可以认为已经100%零配置;
六、设计思想
Struts2更加符合OOP的编程思想, SpringMVC就比较谨慎,在servlet上扩展;
七、集成方面
SpringMVC集成了Ajax,使用非常方便,只需一个注解@ResponseBody就可以实现,然后直接返回响应文本即可,而Struts2拦截器集成了Ajax,在Action中处理时一般必须安装插件或者自己写代码集成进去,使用起来也相对不方便;
什么是session
session在计算机中,尤其是在网络应用中,称为”会话控制“。Session对象存储特定用户会话所需的属性及配置信息。
这样,当用户在应用程序的web页面之间跳转时,存储在session对象中的变量将不会 丢失,而在整个用户会话中一直存在下去;
产生session不一致原因
单台tomcat没有任何问题,但现在是集群的tomcat因此就存在session不一致问题。如
解决方案
(1)session复制
tomcat的session复制,可以实现session共享
优点:不需要额外开发,只需搭建tomcat集群即可
缺点:tomcat 是全局session复制,集群内每个tomcat的session完全同步(也就是任何时候都完全一样的) 在大规模应用的时候, 用户过多,集群内tomcat数量过多,session的全局复制会导致集群性能下降, 因此,tomcat的数量不能太多,5个以下为好;
(2)session绑定
当用户A第一次访问系统时,tomcat1对其进行服务,那么,下次访问时仍然让tomcat1对其进行服务;
(3)使用redis集中管理session
可以将用户的会话保存在redis中,每次从redis中查询用户信息,就可以很好的解决会话共享问题;
实际应用
(1)用户登录问题
对于大型分布式系统,可以使用单点登录系统进行登录,其中用户的session保存在redis缓存系统中;
(2)用户短信验证
当需要对用户短信进行校验前,调取第三方服务获取验证码,需要先将验证码保存在session中,然后与用户提交的验证码进行比对;
GC 是垃圾收集的意思(GabageCollection),内存处理是编程人员容易出现问 题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目 的,Java 语言没有提供释放已分配内存的显示操作方法。
三个步骤:
回收那些不可能再被任何途径使用的对象
1.1.怎样确定哪些对象是不再被引用的对象?(两次标记)
**首先:**通过可达性分析算法对GC-ROOT对象向下搜索,搜索到的对象到GC-ROOT的路径被称为引用链,如果一个对象没有到达GC-ROOT的引用链,则此对象被标记。
然后: 对已标记的对象进行筛选,筛选的条件是对象是否有必要执行finalized()方法。当对象没有覆盖finalized()方法,或者虚拟机已经调用过此方法,都被视为“没有必要执行”。
**最后:**若对象被判定为有必要执行finalized()方法,这些对象将被放置在F-Queue队列中,进行第二次标记。此时虚拟机自动创建的Finalizer线程来会出发finalized()方法,若对象在finalized()方法中逃逸,此对象将在第二次标记时被移除“即将回收”的集合,如果还没有逃脱,对象就被回收。
2.什么时候回收?
- 强引用:普遍存在的对象,类似new对象创建,只要强引用存在,垃圾回收器永远不会回收被引用的对象。
- 软引用:还有用但是非必须,在系统发生内存溢出之前之前,会将这部分对象列入回收范围中进行二次回收,若这轮垃圾回收之后内存还是不够用,则抛出内存溢出异常。(JDK1.2)
- 弱引用:非必须对象,被此关联的对象的生命周期只到下次垃圾回收之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象(JDK1.2,WeakReference类)
- 虚引用:不影响对象的生命周期,并且此引用将不会创建对象。只是在垃圾回收后有系统通知。
3.如何回收?
标记-清除算法
复制算法
标记整理算法
不可变数据类型: 当该数据类型的对应变量的值发生了改变,那么它对应的内存地址也会发生改变,对于这种数据类型,就称不可变数据类型。其中基本数据类型都是不可变数据类型,例如int,如果一个int类型的数据发生改变,那么它指向了内存中的另一个地址,但是需要注意的是java缓存了所有-128-127的值。
可变数据类型 :当该数据类型的对应变量的值发生了改变,那么它对应的内存地址不发生改变,对于这种数据类型,就称可变数据类型,当可变数据类型改变时它实际上是更改了内存中的内容。
String是不可变类型。这里的不可变是指,当编译器开辟堆内存来存储String内容后,这个堆内存无法再被外界修改;重新将引用指向一个新地址,新地址中为更改后的值。 可变数据类型则在原来的地址上直接更改对象值;
程序员可以创建一个String类型变量,通过赋值的方式,使之指向不同的堆内存,从而产生String字符串可变的假象。
停止一个线程通常意味着在线程处理任务完成之前停掉正在做的操作,也就是放弃当前的操作;
在 Java 中有以下 3 种方法可以终止正在运行的线程:
一、使用终止位终止线程
在 run() 方法执行完毕后,该线程就终止了。但是在某些特殊的情况下,run() 方法会被一直执行;比如在服务端程序中可能会使用 while(true) { ... }
这样的循环结构来不断的接收来自客户端的请求。此时就可以用修改标志位的方式来结束 run() 方法。
? //volatile修饰符用来保证其它线程读取的总是该变量的最新的值 public volatile boolean exit = false;
? 将true拿出来设置为一个变量,使程序运行结束后动态的变为false,从而结束程序,同时也达到了终止线程的目的;
二、使用stop()
通过查看 JDK 的 API,我们会看到 java.lang.Thread 类型提供了一系列的方法如 start()、stop()、resume()、suspend()、destory()等方法来管理线程。但是除了 start() 之外,其它几个方法都被声名为已过时(deprecated)。
虽然 stop() 方法确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且该方法已被弃用,最好不要使用它;
为什么弃用stop:
例如,存在一个对象 u 持有 ID 和 NAME 两个字段,假如写入线程在写对象的过程中,只完成了对 ID 的赋值,但没来得及为 NAME 赋值,就被 stop() 导致锁被释放,那么当读取线程得到锁之后再去读取对象 u 的 ID 和 Name 时,就会出现数据不一致的问题;
三、使用interrupt()中断线程
从表面现像上面看 GET 和 POST 的区别:
get⽅式 参数在地址栏中显示 通过?name=""&id=""这种形式传递的 不安全 只能传递2kb的能容 post⽅式 底层是通过流的形式传递 不限制⼤⼩ 上传的时候必须⽤Post⽅式 doGet:路径传参。效率⾼,安全性差 doPOST:实体传参。效率第,安全性好;
in()适合B表比A表数据小的情况;
exist()适合B表比A表数据大的情况;
当A表与B表数据一样大时,二者没有区别;
数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据两比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,增加、插入、删除数据效率比较低;
链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任意的位置,通过应用来关联数据(就是通过存在元素的指针来联系);
按照读写的单位大小来分:
字符流 :以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。 (Java代码接收数据为一般为 char数组,也可以是别的 )
字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据,图片、文件、音乐视频等。 (Java代码接收数据只能为 byte数组 )
按照实际IO操作来分:
输出流:从内存读出到文件。只能进行写操作。
输入流:从文件读入到内存。只能进行读操作。
注意:输出流可以帮助我们创建文件,而输入流不会;
IO
Java中I/O是以流为基础进行数据的输入输出的,所有数据被串行化(所谓串行化就是数据要按顺序进行输入输出)写入输出流。简单来说就是java通过io流方式和外部设备进行交互;在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,网络上的数据传输流,字符串流,对象流等等;
BIO
BIO同步并阻塞,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作(如果是单线程的情况下,我传输的文件很大呢?),当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解;
NIO
NIO同步非阻塞,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持;
AIO
AIO异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开始支持;
BIO、NIO和AIO的区别
Feign可以帮助我们实现面向接口编程,它使得写Http客户端变得更简单;使用Feign只需要创建一个接口并注解。它具有可插拔的注解特性,可使用Feign注解和JAX-RS注解。Feign默认集成了Ribbon和Eureka结合实现了负载均衡的效果;
1、添加Feign的依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
2、修改启动文件
在启动文件中加入@EnableEurekaClient用于启动Eureka客户端注册功能,@EnableFeignClients用于开启feign客户端
3、编写接口(最重要)
我觉得主要是想问FeignClient这个注解
@FeignClient("producer-service1") //设置调用的服务名称
public interface FeignRemoteService {
@RequestMapping(value = "/proMsg",method = RequestMethod.GET)
public String remoteMsg(@RequestParam(value = "msg") String msg);
}
Nginx是一个 轻量级/高性能的反向代理Web服务器,他实现非常高效的反向代理、负载平衡,他可以处理2-3万并发连接数,官方监测能支持5万并发,现在中国使用nginx网站用户有很多,例如:新浪、网易、 腾讯等;
之所以用Nginx是因为跨平台、配置简单、方向代理、高并发连接:处理2-3万并发连接数,官方监测能支持5万并发,内存消耗小:开启10个nginx才占150M内存 ,nginx处理静态文件好,耗费内存少,而且Nginx内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上;
使用Nginx的话还能:
优点:
缺点:
动态处理差:nginx处理静态文件好,耗费内存少,但是处理动态页面则很鸡肋,现在一般前端用nginx作为反向代理抗住压力;
git status :查看变更情况
git add . :将当前目录及其子目录下所有变更都加入到暂存区
git add -A :将仓库内所有变更都加入到暂存区
git add 文件1 文件2 文件3 :将指定文件添加到暂存区
git diff :比较工作区和暂存区的所有差异
git commit -m "提交描述信息" :提交文件
git branch -v :查看当前工作分支及本地分支
git branch -rv :查看远端分支
git checkout 指定分支:切换到指定分支
git remote add origin 远程仓库的地址:关联远程仓库
git remote -v :查看远程仓库地址
git push -u origin master:将本地的master分支上传到远程的master分支上
git clone 克隆的远程仓库地址: 克隆远程仓库
git pull origin master :拉取远程仓库代码
git merge 分支a :分支合并
RabbitMQ是一个在AMQP协议标准基础上完整的,可服用的企业消息系统,遵循Mozilla Public License开源协议,服务器端用Erlang语言编写,支持多种客户端的工业级的消息队列(MQ)服务器,RabbitMQ 是建立在Erlang OTP平台上;
MQ有很多优点:
倒排索引是相对于正排索引而言的,可以有效的解决该问题;
倒排索引的过程:
倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构;
(1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;
(2)查询速度快。O(len(str))的查询时间复杂度;
Ik分词器有ik_max_word和ik_smart两种模式
父组件给子组件传值
1.父组件调用子组件的时候动态绑定属性
<parent :dataList='dataList'>parent>
2.子组件定义props接收动态绑定的属性
props: ['dataList']
3.子组件使用数据
子组件主动获取父子间的属性和方法
在子组件中使用this.$parent.属性
或者this.$parent.方法
子组件给父组件传值
1、使用ref属性
父组件调用子组件时绑定属性ref
<parent :ref='parent'>parent>
在父组件中使用this. r e f s . p a r e n t . 属 性 / t h i s . refs.parent.属性/this. refs.parent.属性/this.refs.parent.方法
2、使用$emit方法
3、vue页面级组件之间传值
共同点:都能控制元素的显示和隐藏;
不同点:实现本质方法不同;
总结:如果要频繁切换某节点,使用v-show(切换开销比较小,初始开销较大)。如果不需要频繁切换某节点使用v-if(初始渲染开销较小,切换开销比较大);
接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created中;
在微服务启动后每过5秒,会由微服务内置的 Nacos 客户端主动向 Nacos 服务器发起心跳包(HeartBeat)。心跳包会包含当前服务实例的名称、IP、端口、集群名、权重等信息;
naming模块心跳处理流程
到这里一次完整的心跳包处理已完成。
处理不健康心跳
当出现无效实例Nacos是咋么剔除的?Nacos Server内置的逻辑是每过 20 秒对“实例 Map”中的所有“非健康”实例进行扫描,如发现“非健康”实例,随即从“实例 Map”中将该实例删除;
详见第71条;
Collection
list:ArrayList、LinkedList、Vector
set:HashSet、TreeSet
Map
HashMap、HashTable、TreeMap
正向代理
一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理;
正向代理总结就一句话:代理端代理的是客户端;
反向代理
反向代理(Reverse Proxy)方式是指以代理服务器来接受 internet 上的连接请求,然后将请求,发给内部网络上的服务器并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器;
反向代理总结就一句话:代理端代理的是服务端;
Swagger 广泛用于可视化 API,使用 Swagger UI 为前端开发人员提供在线沙箱。Swagger 是用于生成 RESTful Web 服务的可视化表示的工具,规范和完整框架实现。它使文档能够以与服务器相同的速度更新。当通过 Swagger 正确定义时,消费者可以使用最少量的实现逻辑来理解远程服务并与其进行交互。因此,Swagger消除了调用服务时的猜测;
publicvoidtestprint(Filefile){
Stringname=file.getName();
booleanb=file.isDirectory();//判断是否为文件夹
if(b){
//是文件夹
Filefiles[]=file.listFiles();
for(Filef:files){
testprint(f);
//System.out.println(name);//打印文件夹名字
}
}else{
System.out.println(name);
}
}
publicstaticvoidmain(String[]args){
Filefile=newFile("d:\\");
testfilefi=newtestfile();
fi.testprint(file);
}
C/S 是 Client/Server 的缩写。服务器通常采用高性能的 PC、工作站或小型机,并采用大型数据库系统,如 Oracle、Sybase、InFORMix 或 SQLServer。客户端需要安装专用的客户端软件;
B/S是 Brower/Server 的缩写,客户机上只要安装一个浏览器(Browser),如 NetscapeNavigator 或 InternetExplorer,服务器安装 Oracle、Sybase、InFORMix 或 SQLServer 等数据库。在这种结构下,用户界面完全通过 WWW浏览器实现,一部分事务逻辑在前端实现,但是主要事务逻辑在服务器端实现。浏览器通过WebServer 同数据库进行数据交互;
C/S 与 B/S 区别:
1、硬件环境不同
2、对安全要求不同
3、对程序架构不同
大概分为12个步骤
UML 是统一建模语言(Unified Modeling Language)的缩写,发表于1997年,综合了当时已经存在的面向对象的建模语言、方法和过程,是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持。使用 UML 可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系统的结构和行为;
UML 定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结构,包括:用例图(use case diagram)、类图(class diagram)、时序图(sequencediagram)、协作图(collaboration diagram)、状态图(statechart diagram)、活动图(activity diagram)、构件图(component diagram)、部署图(deployment
diagram)等。用例图、类图、时序图这三种图最为重要;
用例图
用来捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系;
类图
描述类以及类与类之间的关系,通过该图可以快速了解系统;
时序图
描述执行特定任务时对象之间的交互关系以及执行顺序,通过该图可以了解对象能接收的消息也就是说对象能够向外界提供的服务;
历时2个月整理出来的干货,还是有很多想要分享的但这篇文章篇幅已经很长了,我后期还会不断的补充的;其实我认为在面试的时候除了自身的知识储备还有一点就是态度,态度决定成败,在整个面试过程中表现的很自信也是有必要的但不是过度自信;
最后祝大家能成功上岸!!!