一、Java基础
1.JDK 和 JRE 有什么区别?
- JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
- JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。
- JDK 其实包含了 JRE,运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
2.== 和 equals 的区别是什么?
(1)==
- 基本类型:比较的是值是否相同,他们存储在jvm的栈中,比较的是内容
- 引用类型:比较的是引用(hashCode)是否相同,对象内容存储在jvm的堆中,比较的是两个对象的地址
- 代码
public class test {
public static void main(String[] args) {
Integer a = 1;
Integer b = 1;
System.out.println(a == b);
Integer i1 = new Integer(1);
System.out.println(a == i1);
}
}
- 代码解读
1.Integer是int的封装类,是引用类型,所以==比较的是地址,a和b指向的是同一个引用,地址相同,所以 a==b 结果是true.
2.Integer经过new之后,相当于重新开辟了内存空间,此时i1的引用和原来的a,b不相同,所以内存地址不一样,尽管值相同,根据上述,引用类型比较地址,a和i1地址不同,结果为false
(2).equals
- equals本质就是 “==”
- 一些类重写了equals方法,把它变成了值比较,比如Integer和String
class Cat {
public Cat(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Cat c1 = new Cat("LBY");
Cat c2 = new Cat("LBY");
System.out.println(c1.equals(c2)); // false
public class test {
public static void main(String[] args) {
String s1 = new String("LBY");
String s2 = new String("LBY");
System.out.println(s1.equals(s2)); // true
}
}
3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
- 不对,两个对象的 hashCode()相同,equals()不一定 true。
- 两个对象的equals()相同,hashCode()一定 为true。
4.hashCode()和equals的关系
- hashCode是本地方法,navicat修饰的
- 重写equals方法,建议一起重写hashCode方法
在equals中源码的注释上,有一句相同对象必须有相同哈希值,所以我们在开发的时候应该遵守这个约定,所以equals判断两个对象相等时,hashCode也应该相等
- 为什么要有这个约定呢,在源码上还提到了HashMap
通过阅读HashMap的源码发现,在HashMap存储键值对(key,value)的时候,对象存存储在HashMap集合数组的下标的计算方式是通过HashCode计算的,所以hashCode的值会影响HashMap集合中存放对象的方式,
- 总结
hashCode方法不仅仅是与equals配套使用的,它甚至是与Java集合配套使用的,为了避免代码出现问题,还是尽量按照规定办事儿
5.hashCode()
- hashCode是通过hash算法计算出来的,比如取余算法
- 每个对象都对应一个hashCode,对象是根据hashCode存放在hash表中的
- 例如hash表中有5个hashCode,分别是0,1,2,3,4 五个位置
- 现在要将1-20不一样的数字存放在hash表中
那么根据取余算法
数字1存放位置为 1 % 5 = 1
数字1存放位置为 2 % 5 = 2
数字1存放位置为 3 % 5 = 3
数字1存放位置为 4 % 5 = 4
数字1存放位置为 5 % 5 = 0
数字1存放位置为 6 % 5 = 1
数字1存放位置为 7 % 5 = 2
数字1存放位置为 8 % 5 = 3
数字1存放位置为 9 % 5 = 4
数字1存放位置为 10 % 5 = 0
数字1存放位置为 11 % 5 = 1
数字1存放位置为 12 % 5 = 2
数字1存放位置为 13% 5 = 3
数字1存放位置为 14 % 5 = 4
数字1存放位置为 15 % 5 = 0
数字1存放位置为 16 % 5 = 1
数字1存放位置为 17 % 5 = 2
数字1存放位置为 18 % 5 = 3
数字1存放位置为 19 % 5 = 4
数字1存放位置为 20 % 5 = 0
此时0位置存放的数字有:5,10,15,20
此时1位置存放的数字有:1,6,11,16
此时2位置存放的数字有:2,7,13,17
此时3位置存放的数字有:3,8,12,18
此时4位置存放的数字有:4,9,14,19
- 使用hash算法方式的好处是,存取都比较方便,因为要存放不同的数字,如果使用比较笨的方法,每次存放数字的时候都要和之前的所有数字进行重复比较,存放100个数字的时候,当存放到低100个时,就需要和前面的99个数字注意遍历进行比较,这显然时非常浪费时间的,而使用的hash算法,就可以将需要存储的数字进行hash运算,看一个这个数字应该对应hash表的位置,比如按照上面的案例,存储19的,对19 % 5 = 4,就可以找到hash表中hashCode为4的位置,这时发现④的位置中的数字只有4,9和14,这样只需要将19和第④个位置中的3个数字进行比较,而不是和前面所有的数字逐一进行遍历比较,这样显然提高了效率。
6.HashMap
(1)存储数据的过程
- 原理:jdk7之前数组+链表,jdk1.8数组+链表+红黑树(当数组上的链表的长度大于8的时候就会转换成红黑树,小于6的时候就会重新转变回链表)
- hashMap存储对象的过程
-
存储一个key,value时,首先计算key的hash值,根据hash值和数组的长度,确定插入数组的位置
-
但是可能存在同一个hash值对应的数组位置已经有对象插入了
-
这时就添加到同一hash值元素的后面,他们在数组的同一位置,这样就形成了链表
-
jdk8中,当链表的长度大于8时,链表就转化为红黑树,这样有大大的提高了查找的效率
-
- hashMap在put的时候才会去初始化数组,类似于懒加载,只有在用到的时候才会进行初始化
- hashMap中数组默认的长度是16,扩容因子是0.75,当然可以调用hashMap的构造方法进行传递自己想要的数组长度和扩容因子,但是会进行2的指数次幂进行处理,比如传递的长度是7,那么大于等于7的2的指数值为8(2的3次方)
- hashMap扩容的条件为 数组被填充的长度 >= 数组长度是数组的长度*扩容因子(默认是 16*0.75 = 12,不传值的情况下在数组的长度>=12就会扩容),扩容为原来的两倍,扩容比较耗费时间,本质是定义一个个更大的数组,将老数组的内容克隆到新的数组中。
- jdk为了使散列效果更好,进行了两次散列处理
- 早期的HashTable使用的就是hash值 = hashCode % 数组的长度,这种算法可以使hash值均匀的分布在数组长度范围之间,但是因为取余效率比较低下,jdk改进了算法,通过将数组长度的参数进行2的指数次幂进行处理,在进行一些或和移位运算,同样可以实现取余的效果,而且这种方式的效率更高。
public class Test03 {
public static void main(String[] args) {
int h = 25860399;
int length = 16;//length为2的指数次幂
myHsah(h,length);
}
public static int myHsah(int h,int length){
System.out.println(h&(length-1));
System.out.println(h%length);
return h&(length-1);
}
}
(2)取数据的过程
map.get(key)
- 获得key的hashCode
- 在通过hashCode计算出hash值,进而确定对象存放在数组的位置
- 找到数组的存放的位置之后,在和当前数组上链表上的key挨个进行比较,通过equals()方法,将key对象和链表上所有节点的key进行比较,知道碰撞到返回true的节点对象位置
- 返回equals()为true的节点的value对象
- 此处更加验证了equals和hashCode的关系,因为无论在HashMap上存储对象还是取对象,首先都要进行hashCode的运算,如果两个对象通过equals判断返回true,但是hashCode不相同,那么在HashMap上的存取就会混乱,产生了悖论
(3)扩容
其中参数initialCapacity为初始容量,loadFactor为加载因子,扩容就是在put加入元素的个数超过initialCapacity * loadFactor的时候就会将内部Entry数组大小扩大至原来的2倍,然后将数组元素按照新的数组大小重新计算索引,放在新的数组中,同时修改每个节点的链表关系(主要是next和节点在链表中的位置)。所以数组扩容是非常耗费时间的
(4)为什么要用数组加上链表
数组是一个连续的空间,查询比较快,但是添加删除速度比较慢
链表不是连续的空间,查询速度慢,但是添加和删除速度比较快
所以将两者的优点结合起来就是Hash表
- 举例两种极端的算法,比如,
- 效率非常差的hash算法,hashCode/hashCode,值永远是1,那么数组+链表就退化成了一个链表
- 没有hash算法,只有hashCode,就那就是所有的对象挨个的在数组一次排列,那么数组+链表就退化成了数组,这样空间很快就消耗完了
- 所以两者取中间,数组不能无限大,对象的存储也尽量散列开
(5)Hash冲突
key经过hash算法计算出的hash值,在数组中对用的位置已经有值了,这种情况被程为hash冲突,也就是说两个不同的对象的经过hashCode相同,这种情况就是hash冲突
(6)解决hash冲突
hashMap解决hash冲突的办法就是链地址法,将计算出hash值相同的不同对象,通过链表的方式存放在数组里面
(7)hashMap线程不安全问题
jdk7采用的链表头插法的方式,在多线程并发的情况下,会发生环形链表,或数据丢失的情况
jdk8采用尾插法,不会形成环形链表,但是会出现数据覆盖的情况,所以线程依然不安全
HashMap并不是线程安全的,支持K和V为null ,k重复会覆盖,V可以重复,还有一点HashMap遍历的数据不是有序的是无序的
(8)解决hashMap线程不安全的方法
- 使用Hashtable替代HashMap,Hashtable内方法上使用了synchronized。
- ConcurrentHashMap,方法内部使用了synchronized保证线程安全。【*】推荐使用
- Collections 类的synchronizedMap(Map<K,V> m)方法可以返回一个线程安全的Map
- ConcurrentHashMap生成的Map性能是明显优于Hashtable和Collections 的synchronizedMap()方法。
7.final 在 java 中有什么作用?
- final修饰类,不能被继承
- final修饰方法,不能被重写,编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。
- final修饰变量,不能被修改
扩展:final空白,就是在类中定义一个没有赋初始值的final的变量,但是前提是需要提供一个只有当前没有复制final修饰参数的构造方法,并且不能有其他的任何构造方法,目的就是根据对象的不同,final修饰的属性值不同
8.java 中的取整
方法一:向上取整Math.ceil();
举例:Math.ceil(11.4)=12; Math.ceil(-11.6)=-11;
方法二:向下取整Math.floor();
举例:Math.floor(11.7)=11;Math.floor(-11.2)=-12;
方法三:四舍五入Math.round();
顾名思义,四舍五入后取整,其算法为Math.round(x+0.5),即原来的数字加上0.5后再想下取整即可。
举例:Math.round(11.5)=12;
Math.round(-11.5)=-11;
9.String 属于基础的数据类型吗?
String 不属于基础类型,基础类型有 8 种:byte、boolean、char、short、int、float、long、double,而 String 属于对象。
10.java 中操作字符串都有哪些类?它们之间有什么区别?
操作字符串的类有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象(String类是被final修饰的),每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
11.jvm
(1)jvm内存结构图示
(2)总结
栈:
- 栈是方法执行的内存模型,也就是方法执行的调用都在栈里面进行
- 每个方法被调用都会在栈里面开辟一个内存空间创建一个栈帧,方法调用完毕会销毁
这就是出栈入栈的过程,入栈出栈的原则是先进后出,后进先出,就像枪打子弹一样,先放进去的子弹最后打出,最后放进区的子弹最先被打出
- jvm会为每一个线程创建一个栈,栈属于线程私有,不能在线程之间共享
- 栈是由系统自动分配,速度快,是一个连续的内存空间
- 栈存储的内容:
- 基本类型 局部 变量(8种基本数据类型定义的变量:char c,short s,int a,long l,float f,double d.....),存储的就是c,s,a,l,f,d
- 基本类型局部变量的值(char c = 'c',int i = 1),存储的就是c,1
- 对象的引用,(Testq q = new Test(),Cat myCat = new myCat();)中的 q 和 myCat存储在栈中
堆:
- jvm中只有一个堆,在虚拟机启动时创建
- GC 管理的主要区域。
- 被线程共享,此内存的唯一目的就是存放对象实例和数组
- 堆是一个不连续的把内存空间,但是分配灵活,速度慢
- 为程序员动态分配的内存,大小不定也不会自动释放,特点是先进先出
- 堆中存储的内容
- 对象,对象中的基本信息作为一个整体被存储,例如成员变量等
- 方法区也在堆中
方法区
- 方法区在堆中,所以也被线程共享
- 用于储存已被虚拟机加载的类信息(class字节码)、静态变量和方法(static修饰的)
- 常量池在方法区中(jdk6)
常量池
- 又称永久代
- 方法区的一部分
- 存放字符串常量和基本数据类型常量(final)
【注】常量池在JDK1.6的时候,放在方法区中。到了JDK1.7,转移到了堆区。到JDK8时取而代之的是元空间。
【注】类的方法只有一套,被所有类的对象所共享,只有使用方法的时候才会入栈,进行内存的分配,不使用方法不占用内存
(3)垃圾回收GC
- GC是JVM的核心组件
- 它在JVM中以单独的线程(daemon thread)运行
- 作用于内存堆区域(Stack Space),也会扫描方法区(永久代)
- 扫描那些经过new关键字创建的无用的对象并清除以释放内存,必要时整理内存。
年轻代
- 年轻代的内存分为三个部分,分别是Eden,Survivor1和Survivor2,比例分别是8:1:1
- 所有新生成的对象首先都是放在Eden区
- 年轻代的目标就是尽可能快速的回收掉那些生命周期短的对象(回收不掉的说明还在使用的对象,是存活对象)
- 回收年轻代的是Minor GC,会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间
- 当“年轻代”区域存放满对象后,就将对象存放到年老代区域。
- 主要使用复制算法
年老代
- 在年轻代中经历了N(默认15,对象从survivor1到survivor2有15次)次垃圾回收后仍然存活的对象,就会被放到年老代中。
- 年老代中存放的都是一些生命周期较长的对象。
- 年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),全面清理年轻代区域和年老代区域.
- 主要使用标记-清理或标记-整理算法.
持久代
- 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。
堆内存的划分细节:
Minor GC/Scavenge GC:(次收集)
- 用于清理年轻代区域无用对象。
- 触发条件:Eden区满了就会触发一次Minor GC,新创建的对象大小 > Eden所剩空间
- 将有用对象复制到“Survivor1”、“Survivor2”区中
Full GC/Magor GC:(全收集)
- 用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
- 每次晋升到老年代的对象平均大小>老年代剩余空间
12.垃圾回收过程:
[注]下列图示中的蓝色区域代需要被回收的对象占用的内存空间,黄色为存货对象占用的内存空间
1、新创建的对象,绝大多数都会存储在Eden中,
2、当Eden第一次满了(达到一定比例)不能创建新对象,则触发垃圾回收(GC),将无用对象清理掉,
然后剩余对象复制到某个Survivor中,如S1,同时清空Eden区
3、当Eden区第二次满了,会回收掉Eden和Suervivor1中没用的对象占用的空间,并且将Eden和Survivor1中存活对象存到Survivor2中
4、当Eden区第三次满了,会将回收到Edon和Suervivor2中没用的对象占用的空间,并且将Eden和Survivor2中存活对象存到Survivor1中
4、重复多次(默认15次)Survivor中没有被清理的对象,则会复制到老年代Old(Tenured)区中,
5、当Old区满了,则会触发一个一次完整地垃圾回收(FullGC),之前新生代的垃圾回收称为(minorGC)
13.VM堆区新生代为什么有两个Survivor
- 参考博文地址: https://www.jianshu.com/p/3d3fc356e31c
- 两个survivor大小相同
- survivor用到了复制算法
- 同一时刻Survivor1和Survivor2只有一个在用,一个为空
- 原因:如果只使用一个survivor,会产生内存碎片
- 图解
- 只有一个survivor区垃圾回收的情况前后对比
- 通过上面两张图片可以看出,垃圾回收产生了内存碎片
- 当然只是用一个survivor区时,可以通过对象在堆中重新排列组合的方法来实现防止碎片化的问题,但是那么多的对象进行排列组合,会耗费大量的时间,而使用两个survivor区,在垃圾回收的过程当中,总有一个survivor区时空白的,也就是没有使用的,但是实现了防止内存碎片化的问题,所以两个survivor其实就是牺牲空间来换取时间
14.垃圾回收算法
分代垃圾回收机制是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。Java虚拟机将对象分为三种状态:年轻代、年老代、持久代。JVM将堆内存划分为 Eden、Survivor 和 Tenured/Old 空间。
1、引用计数(Reference Counting)
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
2、复制(Copying)
- 概念
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间,年轻代中的survivor区使用的就是复制算法
- 图示
[注]橙色蓝色和白色代表未被使用的空间,灰色代表需要被回收的空间,绿色代表需要被复制的空间
3、标记-清除(Mark-Sweep)
- 概念
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
- 图示
- 总结
就像一张由对象组成的关系网,从第一个创建并放在这张关系网上的对象开始,所有在这张网上有引用关系的对象都会被标记,没有被标记的对象说明没有被引用,那么这个没有被引用的对象就是需要被垃圾回收的对象,将需要被回收的对象占用的空间清除之后,会产生空间碎片,且此算法会暂停整个应用。
4、标记-整理(Mark-Compact)
- 概念
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。简图如下:
- 图示
- 总结,分两步
- 第一步:清除没有被引用的对象占用的空间
- 第二步:将存活的对象,按照顺序压缩到一起
15.垃圾收集器
jvm有7个垃圾收集器
- 年轻代有3个
- 串行收集器(Serial)
- 并行收集器(ParNew)
- Parallel Scavenge收集器
- 年老代3个
- Serial Old收集器
- Parallel Old收集器
- CMS收集器(Concurrent Mark Sweep)
- 年轻代年老代都可以垃圾回收
- G1收集器
16.如何将字符串反转?
- StringBuilder 或者 stringBuffer 的 reverse() 方法。
// StringBuffer reverse
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("lby");
System.out.println(stringBuffer.reverse()); // gfedcba
// StringBuilder reverse
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("lby");
System.out.println(stringBuilder.reverse()); // gfedcba
17.String 类的常用方法都有那些?
- indexOf(String str):返回指定字符的索引,如果有多个默认第一个
- charAt(int index):返回指定索引处的字符。
- replace(char oldChar, char newChar):字符串替换。
- trim():去除字符串两端空白。
- split(String regex):分割字符串,返回一个分割后的字符串数组。
- getBytes(String charsetName):返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring(int beginIndex, int endIndex)/substring(int beginIndex):截取字符串。
- equals():字符串比较。
18.抽象类
(1)必须要有抽象方法吗?
抽象类不一定需要抽象方法,但是有抽象方法的类一定是抽象类
(2)普通类和抽象类有哪些区别?
- 普通类不能包含抽象方法,抽象类可以包含普通方法,有抽象方法的类一定是抽象类
- 抽象类不能直接实例化,普通类可以直接实例化。
(3)抽象类能使用 final 修饰吗?
不能,因为抽象类的目的就是要给其他类区继承,从而达到实现多态的目的,使用了final代表这个类不可以被继承,那么抽象类的目的就已经失效了。
(4)接口和抽象类有什么区别?
- 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
- 构造函数:抽象类可以有构造函数;接口不能有。
- main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法(jdk8可以)。
- 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
- 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
19.JDK8接口有什么变化
在jdk8之前,interface之中可以定义变量和方法,变量必须是public、static、final的,方法必须是public、abstract的。
JDK8及以后,允许我们在接口中定义static方法和default方法
原因:如果接口中只能定义抽象方法,当修改接口时,该接口的所有实现类都会受到影响,为了减小这种影响,所以才采用了这种方式
19.IO
(1).java 中 IO 流分为几种?
- 按功能来分:输入流(input)、输出流(output)。
- 按类型来分:字节流和字符流。
- 字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
(2).BIO、NIO、AIO 有什么区别?
- BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
- NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
- AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
(3).Files的常用方法都有哪些?
- Files.exists():检测文件路径是否存在。
- Files.createFile():创建文件。
- Files.createDirectory():创建文件夹。
- Files.delete():删除一个文件或目录。
- Files.copy():复制文件。
- Files.move():移动文件。
- Files.size():查看文件个数。
- Files.read():读取文件。
- Files.write():写入文件。
20、集合和数组的区别
(1).相同点
- 集合和数组都是Java中的容器
(2).区别
- 集合的长度可变,数组的长度固定
(3).集合的结构
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap
21.重要集合的介绍和区别
1.List和Set的区别
①List:元素不唯一,有序(索引顺序)
②Set:元素唯一,无序
③Map:存储一组键值对,提供key、value映射
- key:唯一,无序
- value:不唯一,无序
2.List
- ArrayList:
- 底层数据结构是数组(实现了数组长度可变),线程不安全,默认长度为10,每次扩容原来的一半
- 优点:查询快,增删慢
- 缺点:因为增删需要一定大量的元素所以效率低,存放的数据是连续的,所以在查询的时候通过数组的首地址+索引就可以直接计算出对应位置的元素,所以速度比较快
- LinkedList
- 采用双向链表存储方式
- 缺点:遍历和随机访问比较慢,存放的元素不连续,查询的时候只能通过首元素的地址依次获取下一个元素的地址
- 优点:插入、删除元素效率比较高,因为只需要操作头尾节点即可,不需要移动元素
- 里面的元素分配在内存中是不连续的,而是通过引用互相关联
- 没有初始化大小,也没有扩容机制
- Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素
- 如何解决ArrayList线程不安全问题
- 使用Vector,其在方法上加了锁synchronized,每次只能一个线程进行操作
- Collections.synchronized()
- List list =Collections.synchronizedList(new ArrayList<>());使用Collections集合工具类,在ArrayList前面增加锁(同步)机制。
- CopyOnWriteArrayList
- 读操作时,所有线程读同一条数据,写操作时,会copy出来一个新的模板,进行写的操作,之后将新地址赋值给旧的地址,写的操作时同步的,否则会多个线程下可能出现copy出来多个副本,那样的话线程依然是不安全的
3.Set
-
HashSet
- 采用Hash表存储结构
- 优点:添加、查询速度都很快
- 缺点:无序
-
LinkedHashSet
-
采用哈希表存储结构,同时使用链表维护次序
-
有序(添加顺序)
-
-
TreeSet
-
采用二叉树(红黑树)的存储结构
-
优点:有序 查询速度比List快(按照内容查询)
-
缺点:查询速度没有HashSet快
-
4.Map
- HashMap
- Key无序 唯一 (Set)
-
Value 无序 不唯一 (Collection)
-
LinkedHashMap
-
有序的HashMap 速度快
-
-
TreeMap
-
有序 速度没有hash快
-
-
问题:Set和Map有关系吗
-
采用了相同的数据结构,只用于map的key存储数据,就是Set
-
5.Iterator
所有集合类均未提供相应的遍历方法,而是把把遍历交给迭代器完成。迭代器为集合而生,专门实现集合遍历
- Iterator方法
- boolean hasNext(): 判断是否存在另一个可访问的元素
- Object next(): 返回要访问的下一个元素
- void remove(): 删除上次访问返回的对象。
6.Collections
- Collections.addAll(list, "aaa","bbb","ccc","ccc");
- int index = Collections.binarySearch(list, "ccc");
- Collections.copy(list2, list);
- Collections.fill(list3, "888");
- String max = Collections.max(list4);
- String min = Collections.min(list4);
- Collections.reverse(list4);
- List list5 = Collections.synchronizedList(list4);
22.HashMap的数据结构
(1)数据结构图解
- jdk1.7之前HashMap的基础结构就是数组加链表的格式
- jdk1.8HashMap的基础结构就变成了数组加链表加红黑树的形式,条件是数组上的链表长度超过8
- jdk链表的长度小于6的时候自动转化为链表
- 加了红黑树,在数据量较大且哈希碰撞较多时,能够极大的增加检索的效率
①jdk1.7
②jdk1.8
Hash的存取数据的过程
图解
1.存数据
①hashMap在put的时候才会去初始化数组,类似于懒加载,只有在用到的时候才会进行初始化,HashMap中数组长度默认是16
- 数组的长度是2的幂指数,可以指定数组的长度,但是会自动转换成2的幂指数
- 2的幂数的原因是为了保证Hash值分布的更均匀,2的幂数的值,进行减一后其他位都为1,这样可以保证Hash值进行&运算的值时,散列的更均匀,否则其他位有0的情况下,数组的位置,有一些一直用不到,散列不均匀
- 默认长度16的原因就是经验值,太大了浪费空间,太小会浪费时间,所以一直使用16这个经验值
②通过key计算Hash值,然后和数组的长度减一进行&(与)运算,得到数组的下标
- jdk1.7用的是取余(%)运算,计算的数组下标
- jdk1.8通过&运算,可以达到和%运算的相同效果,因为&运算是通过0/1进行运算,所以速度比较快
- &运算,全1为1,否则为0
- 这样计算的原因就是想
③计算的数组下标中没有对象,直接将当前对象放进数组;有对象,将对象放在前一个对象的后边
④数组上链表的长度大于或者等于8,链表会转化位红黑树,提高了查找的效率
2.取数据
map.get(key)
- 获得key的hashCode
- 在通过hashCode计算出hash值,进而确定对象存放在数组的位置
- 在和当前数组上链表上的key挨个进行比较,通过equals()方法,将key对象和链表上所有节点的key进行比较,直到碰撞到返回true的节点对象位置
- 此处更加验证了equals和hashCode的关系,因为无论在HashMap上存储对象还是取对象,首先都要进行hashCode的运算,如果两个对象通过equals判断返回true,但是hashCode不相同,那么在HashMap上的存取就会混乱,产生了悖论
3.数组扩容
- 扩容就是在put加入元素的个数超过当前(数组的容量)*(扩容因子)的时候就会将内部Entry数组大小扩大至原来的2倍
- 然后将数组元素按照新的数组大小重新计算索引,放在新的数组中,同时修改每个节点的链表关系(主要是next和节点在链表中的位置),所以数组扩容是非常耗费时间的
4.删除元素
- 先通过key的Hash值进行计算,得到数组的位置
- 如果是链表进行链表遍历,通过key进行比对,比对成功之后将元素删除
- 如果是红黑树进行数遍历,删除元素之后进行平衡调节
- 当元素小于6之后,自动转化为链表
5.遍历
(1)四种方式
- 分别遍历key和value的集合
//1、分别获取key和value的集合
for(String key : map.keySet()){
System.out.println(key);
}
for(Object value : map.values()){
System.out.println(value);
}
- 获取 key 集合,然后遍历key集合,根据key分别得到相应value
//2、获取key集合,然后遍历key,根据key得到 value
Set<String> keySet = map.keySet();
for(String str : keySet){
System.out.println(str+"-"+map.get(str));
}
- 得到 Entry 集合,然后遍历 Entry
//3、得到 Entry 集合,然后遍历 Entry
Set<Map.Entry<String,Object>> entrySet = map.entrySet();
for(Map.Entry<String,Object> entry : entrySet){
System.out.println(entry.getKey()+"-"+entry.getValue());
}
- 迭代
//4、迭代
Iterator<Map.Entry<String,Object>> iterator = map.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<String,Object> mapEntry = iterator.next();
System.out.println(mapEntry.getKey()+"-"+mapEntry.getValue());
}
(2)总结
- 基本上使用第三种方法是性能最好的
- 第一种遍历方法在我们只需要 key 集合或者只需要 value 集合时使用;
- 第二种方法效率很低,不推荐使用;
- 第四种方法效率也挺好,关键是在遍历的过程中我们可以对集合中的元素进行删除
23.HashMap基本知识
①允许 key 和 value 都为 null。key 重复会被覆盖,value 允许重复。
②非线程安全
③无序(遍历HashMap得到元素的顺序不是按照插入的顺序)
HashMap相关面试题
- 什么是HashMap
①HashMap其实就是key/value的键值组合,每个键值组合就是一个Entry,这个Entry分散的存储在数组上
- HashMap是如何存数据的
- HashMap是如何取数据的
- HashMap初始长度是多少,为什么这么规定
- 为什么高并发的情况下HashMap会可能出现死锁
①首先HashMap并不是线程同步的
②在扩容的时候,链表的位置会发生反转,这样在高并发的情况下,非常容易链表环,引起死循环
③HashMap并不是线程安全,所以在多线程情况下,应该首先考虑用ConcurrentHashMap。避免悲剧的发生
④jdk7采用的链表头插法的方式,在多线程并发的情况下,会发生环形链表,或数据丢失的情况
⑤jdk8采用尾插法,不会形成环形链表,但是会出现数据覆盖的情况,所以线程依然不安全
- 怎样解决HashMap线程不安全问题
①使用Hashtable替代HashMap,Hashtable内方法上使用了synchronized。
②ConcurrentHashMap,方法内部使用了synchronized保证线程安全。【*】推荐使用
③Collections 类的synchronizedMap(Map<K,V> m)方法可以返回一个线程安全的Map
④ConcurrentHashMap生成的Map性能是明显优于Hashtable和Collections 的synchronizedMap()方法。
- Java8中,HashMap的结构怎样优化
①优化 hash 算法只进行一次位移操作
②引入红黑树,在冲突比较严重的情况下,将 get 操作的时间复杂从 O(n) 降为了 O(logn)
- 什么是Hash冲突
①当key的hash值和数组的长度减一进行与运算的时候,会得到相同的值,这就是hash冲突
- 解决hash冲突
①使用链地址法,将冲突的元素,追加在前面的元素后面,形成链表
24.最最最全面的Java异常面试及解答
作者 | ThinkWon
来源 | blog.csdn.net/ThinkWon/article/details/101681073
Java异常简介
Java异常是Java提供的一种识别及响应错误的一致性机制。
Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。
Java异常架构
1. Throwable
Throwable 是 Java 语言中所有错误与异常的超类。
Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。
Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。
2. Error(错误)
定义:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
特点:此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。
这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的!
3. Exception(异常)
程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
运行时异常
定义:RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。
特点:Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。比如NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、ClassCastException类型转换异常、ArithmeticExecption算术异常。
此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!
RuntimeException 异常会由 Java 虚拟机自动抛出并自动捕获(就算我们没写异常捕获语句运行时也会抛出错误!!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。
编译时异常
定义: Exception 中除 RuntimeException 及其子类之外的异常。
特点: Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常。
4. 受检异常与非受检异常
Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。
受检异常
编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。
非受检异常
编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。往期:一百期面试题汇总
Java异常关键字
-
try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
-
catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
-
finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
-
throw – 用于抛出异常。
-
throws – 用在方法签名中,用于声明该方法可能抛出的异常。
Java异常处理
Java 通过面向对象的方法进行异常处理,一旦方法抛出异常,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常,把各种不同的异常进行分类,并提供了良好的接口。在 Java 中,每个异常都是一个对象,它是 Throwable 类或其子类的实例。
当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java 的异常处理是通过 5 个关键词来实现的:try、catch、throw、throws 和 finally。
在Java应用中,异常的处理机制分为声明异常,抛出异常和捕获异常。
声明异常
通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。
注意
-
非检查异常(Error、RuntimeException 或它们的子类)不可使用 throws 关键字来声明要抛出的异常。
-
一个方法出现编译时异常,就需要 try-catch/ throws 处理,否则会导致编译错误。抛出异常
如果你觉得解决不了某些异常问题,且不需要调用者处理,那么你可以抛出异常。
throw关键字作用是在方法内部抛出一个Throwable类型的异常。任何Java代码都可以通过throw语句抛出异常。
捕获异常
程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。
如何选择异常类型
可以根据下图来选择是捕获异常,声明异常还是抛出异常
常见异常处理方式
直接抛出异常
通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。
private static void readFile(String filePath) throws IOException {
File file = new File(filePath);
String result;
BufferedReader reader = new BufferedReader(new FileReader(file));
while((result = reader.readLine())!=null) {
System.out.println(result);
}
reader.close();
}
封装异常再抛出
有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。
private static void readFile(String filePath) throws MyException {
try {
// code
} catch (IOException e) {
MyException ex = new MyException("read file failed.");
ex.initCause(e);
throw ex;
}
}
捕获异常
在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理
private static void readFile(String filePath) {
try {
// code
} catch (FileNotFoundException e) {
// handle FileNotFoundException
} catch (IOException e){
// handle IOException
}
}
同一个 catch 也可以捕获多种类型异常,用 | 隔开
private static void readFile(String filePath) {
try {
// code
} catch (FileNotFoundException | UnknownHostException e) {
// handle FileNotFoundException or UnknownHostException
} catch (IOException e){
// handle IOException
}
}
自定义异常
习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用)
public class MyException extends Exception {
public MyException(){ }
public MyException(String msg){
super(msg);
}
// ...
}
try-catch-finally
当方法中发生异常,异常处之后的代码不会再执行,如果之前获取了一些本地资源需要释放,则需要在方法正常结束时和 catch 语句中都调用释放本地资源的代码,显得代码比较繁琐,finally 语句可以解决这个问题。往期:一百期面试题汇总
private static void readFile(String filePath) throws MyException {
File file = new File(filePath);
String result;
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
while((result = reader.readLine())!=null) {
System.out.println(result);
}
} catch (IOException e) {
System.out.println("readFile method catch block.");
MyException ex = new MyException("read file failed.");
ex.initCause(e);
throw ex;
} finally {
System.out.println("readFile method finally block.");
if (null != reader) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
调用该方法时,读取文件时若发生异常,代码会进入 catch 代码块,之后进入 finally 代码块;若读取文件时未发生异常,则会跳过 catch 代码块直接进入 finally 代码块。所以无论代码中是否发生异常,fianlly 中的代码都会执行。
若 catch 代码块中包含 return 语句,finally 中的代码还会执行吗?将以上代码中的 catch 子句修改如下:
catch (IOException e) {
System.out.println("readFile method catch block.");
return;
}
调用 readFile 方法,观察当 catch 子句中调用 return 语句时,finally 子句是否执行
readFile method catch block.
readFile method finally block.
可见,即使 catch 中包含了 return 语句,finally 子句依然会执行。若 finally 中也包含 return 语句,finally 中的 return 会覆盖前面的 return.
try-with-resource
上面例子中,finally 中的 close 方法也可能抛出 IOException, 从而覆盖了原始异常。JAVA 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。
private static void tryWithResourceTest(){
try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
// code
} catch (IOException e){
// handle exception
}
}
try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。往期:一百期面试题汇总
24.Java异常常见面试题
1. Error 和 Exception 区别是什么?
Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复;
Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。
2. 运行时异常和一般异常(受检异常)区别是什么?
运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。Java 编译器不会检查运行时异常。
受检异常是Exception 中除 RuntimeException 及其子类之外的异常。Java 编译器会检查受检异常。
RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。
3. JVM 是如何处理异常的?
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
4. throw 和 throws 的区别是什么?
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:
-
throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
-
throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
5. final、finally、finalize 有什么区别?
-
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
-
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
-
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
6. NoClassDefFoundError 和 ClassNotFoundException 区别?
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。
引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;
ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。往期:一百期面试题汇总
7. try-catch-finally 中哪个部分可以省略?
答:catch 可以省略
原因
更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。
理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。
至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。
8. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?
答:会执行,在 return 前执行。
注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。
代码示例1:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
* 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
* 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
*/
} finally {
a = 40;
}
return a;
}
执行结果:30
代码示例2:
public static int getInt() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
} finally {
a = 40;
//如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
return a;
}
}
执行结果:40
9. 类 ExampleA 继承 Exception,类 ExampleB 继承ExampleA。
有如下代码片断:
try {
throw new ExampleB("b")
} catch(ExampleA e){
System.out.println("ExampleA");
} catch(Exception e){
System.out.println("Exception");
}
请问执行此段代码的输出是什么?
答:
输出:ExampleA。(根据里氏代换原则 能使用父类型的地方一定能使用子类型
,抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的异常)
面试题 - 说出下面代码的运行结果。(此题的出处是《Java 编程思想》一书)
class Annoyance extends Exception {
}
class Sneeze extends Annoyance {
}
class Human {
public static void main(String[] args)
throws Exception {
try {
try {
throw new Sneeze();
} catch ( Annoyance a ) {
System.out.println("Caught Annoyance");
throw a;
}
} catch ( Sneeze s ) {
System.out.println("Caught Sneeze");
return ;
} finally {
System.out.println("Hello World!");
}
}
}
结果
Caught Annoyance
Caught Sneeze
Hello World!
10. 常见的 RuntimeException 有哪些?
-
ClassCastException(类转换异常)
-
IndexOutOfBoundsException(数组越界)
-
NullPointerException(空指针)
-
ArrayStoreException(数据存储异常,操作数组时类型不一致)
-
还有IO操作的BufferOverflowException异常
11. Java常见异常有哪些
-
java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
-
java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
-
java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
-
java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
-
java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
-
java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
-
java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。
-
java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
-
java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
-
java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
-
java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
-
java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
-
java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
-
java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
-
java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。
25.异常处理最佳实践
在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。
本文给出几个被很多团队使用的异常处理最佳实践。
1. 在 finally 块中清理资源或者使用 try-with-resource 语句
当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
// do NOT do this
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资源可以正常关闭。
但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的最后部分。结果就是,你并没有关闭资源。
所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。
1.1 使用 finally 代码块
与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资源。
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}
1.2 Java 7 的 try-with-resource 语法
如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
2. 优先明确的异常
你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异常。
因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的处理异常并且避免额外的检查。
因此,总是尝试寻找最适合你的异常事件的类,例如,抛出一个 NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。
public void doNotDoThis() throws Exception {
...
}
public void doThis() throws NumberFormatException {
...
}
3. 对异常进行文档说明
当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。
在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。
public void doSomething(String input) throws MyBusinessException {
...
}
4. 使用描述性消息抛出异常
在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。
但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。
如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被 java.lang.Long 类的构造函数抛出。
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
}
5. 优先捕获最具体的异常
大多数 IDE 都可以帮助你实现这个最佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问的代码块。
但问题在于,只有匹配异常的第一个 catch 块会被执行。因此,如果首先捕获 IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的 catch 块,因为它是 IllegalArgumentException 的子类。
总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。
你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。第一个 catch 块处理所有 NumberFormatException 异常,第二个处理所有非 NumberFormatException 异常的IllegalArgumentException 异常。
public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
6. 不要捕获 Throwable 类
Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做!
如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。
所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。
public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
7. 不要忽略异常
很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。
public void doNotIgnoreExceptions() {
try {
// do something
} catch (NumberFormatException e) {
// this will never happen
}
}
但现实是经常会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。
合理的做法是至少要记录异常的信息。
public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e);
}
}
8. 不要记录并抛出异常
这可能是本文中最常被忽略的最佳实践。可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下:
try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下:
17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装为自定义异常。
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。
9. 包装异常时不要抛弃原始的异常
捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。
在你这样做时,请确保将原始异常设置为原因(注:参考下方代码 NumberFormatException e 中的原始异常 e )。Exception 类提供了特殊的构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难。
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}
10. 不要使用异常控制程序的流程
不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能。
11. 使用标准异常
如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。
12. 异常会影响性能
异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。
-
仅在异常情况下使用异常;
-
在可恢复的异常情况下使用异常;
尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。往期:一百期面试题汇总
13. 总结
综上所述,当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。
异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。
异常处理-阿里巴巴Java开发手册
-
【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException等等。说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。正例:if (obj != null) {…} 反例:try { obj.method(); } catch (NullPointerException e) {…}
-
【强制】异常不要用来做流程控制,条件控制。说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
-
【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
-
【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
-
【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
-
【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。说明:如果JDK7及以上,可以使用try-with-resources方式。
-
【强制】不要在finally块中使用return。说明:try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。反例:
private int x = 0;
public int checkReturn() {
try {
// x等于1,此处不返回
return ++x;
} finally {
// 返回的结果是2
return ++x;
}
}
-
【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
-
【强制】在调用RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException。什么情况会抛出NoSuchMethodError呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。
-
【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。说明:本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。
-
【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景:1) 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。反例:public int f() { return Integer对象}, 如果为null,自动解箱抛NPE。2) 数据库的查询结果可能为null。3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。4) 远程调用返回对象时,一律要求进行空指针判断,防止NPE。5) 对于Session中获取的数据,建议进行NPE检查,避免空指针。6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。正例:使用JDK8的Optional类来防止NPE问题。
-
【推荐】定义时区分unchecked / checked 异常,避免直接抛出new RuntimeException(),更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException等。
-
【参考】对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess()方法、“错误码”、“错误简短信息”。说明:关于RPC方法返回方式使用Result方式的理由:1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
-
【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。正例:一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取:private boolean checkParam(DTO dto) {…}
25.Cookie和Session
【*】会话
-
一次会话包含多次请求和响应,一次会话就是浏览器第一次向服务器资源发送请求,会话建立,知道其中一方断开为止,会话结束
-
功能:在一次会话的范围内的多次请求间,共享数据
-
方式:
1. 客户端会话技术:Cookie
2. 服务器端会话技术:Session
【*】Cookie
1. 概念:
客户端会话技术,将数据保存到客户端
2. 快速入门:
* 使用步骤:
1. 创建Cookie对象,绑定数据
* new Cookie(String name, String value)
2. 发送Cookie对象
* response.addCookie(Cookie cookie)
3. 获取Cookie,拿到数据
* Cookie[] request.getCookies()
3. 实现原理
* 基于响应头set-cookie和请求头cookie实现
4. cookie的细节
1. 一次可不可以发送多个cookie?
* 可以
* 可以创建多个Cookie对象,使用response调用多次addCookie方法发送cookie即可。
2. cookie在浏览器中保存多长时间?
1. 默认情况下,当浏览器关闭后,Cookie数据被销毁
2. 持久化存储:
* setMaxAge(int seconds)
1. 正数:将Cookie数据写到硬盘的文件中。持久化存储。并指定cookie存活时间,时间到后,cookie文件自动失效
2. 负数:默认值
3. 零:删除cookie信息
3. cookie能不能存中文?
* 在tomcat 8 之前 cookie中不能直接存储中文数据。
* 需要将中文数据转码---一般采用URL编码(%E3)
* 在tomcat 8 之后,cookie支持中文数据。特殊字符还是不支持,建议使用URL编码存储,URL解码解析
4. cookie共享问题?
1. 假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?
* 默认情况下cookie不能共享
* setPath(String path):设置cookie的获取范围。默认情况下,设置当前的虚拟目录
* 如果要共享,则可以将path设置为"/"
2. 不同的tomcat服务器间cookie共享问题?
* setDomain(String path):如果设置一级域名相同,那么多个服务器之间cookie可以共享
* setDomain(".baidu.com"),那么tieba.baidu.com和news.baidu.com中cookie可以共享
5. Cookie的特点和作用
1. cookie存储数据在客户端浏览器
2. 浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)
* 作用:
1. cookie一般用于存出少量的不太敏感的数据
2. 在不登录的情况下,完成服务器对客户端的身份识别
6. 案例:记住上一次访问时间
1. 需求:
1. 访问一个Servlet,如果是第一次访问,则提示:您好,欢迎您首次访问。
2. 如果不是第一次访问,则提示:欢迎回来,您上次访问时间为:显示时间字符串
2. 分析:
1. 可以采用Cookie来完成
2. 在服务器中的Servlet判断是否有一个名为lastTime的cookie
1. 有:不是第一次访问
1. 响应数据:欢迎回来,您上次访问时间为:2018年6月10日11:50:20
2. 写回Cookie:lastTime=2018年6月10日11:50:01
2. 没有:是第一次访问
1. 响应数据:您好,欢迎您首次访问
2. 写回Cookie:lastTime=2018年6月10日11:50:01
3. 主要代码功能实现:
//设置响应的消息体的数据格式以及编码
response.setContentType("text/html;charset=utf-8");
//1.获取所有Cookie
Cookie[] cookies = request.getCookies();
if(cookies != null && cookies.length > 0){
//遍历所有的Cookie
for (Cookie cookie : cookies) {
//获取cookie的名称
String name = cookie.getName();
//判断名称是否是:lastTime
if("lastTime".equals(name)){
//将Cookie的Value进行编码
String cookieVal = URLEncoder.encode(str_date,"utf-8");
//获取name为lastTime的Cookie的值
cookie.setValue(cookieVal);
//设置cookie的存活时间
cookie.setMaxAge(60 * 60 * 24 * 30);//一个月
//将服务器生成的Cookie相应给浏览器
response.addCookie(cookie);
}
}
}
//将Cookie进行解码
//获取Cookie的value
String value = cookie.getValue();
//URL解码:
value = URLDecoder.decode(value,"utf-8");
【*】Session
1.概念:服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中。HttpSession
2.快速入门:
1. 获取HttpSession对象:
HttpSession session = request.getSession();
2. 使用HttpSession对象:
Object getAttribute(String name)
void setAttribute(String name, Object value)
void removeAttribute(String name)
3. 原理
* Session的实现是依赖于Cookie的。
4. 细节:
1. 当客户端关闭后,服务器不关闭,两次获取session是否为同一个?
* 默认情况下。不是。
* 如果需要相同,则可以创建Cookie,键为JSESSIONID,设置最大存活时间,让cookie持久化保存。
Cookie c = new Cookie("JSESSIONID",session.getId());
c.setMaxAge(60*60);
response.addCookie(c);
2. 客户端不关闭,服务器关闭后,两次获取的session是同一个吗?
* 不是同一个,但是要确保数据不丢失。tomcat自动完成以下工作(Tomcat通过钝化活化帮助我们解决这个问题)
* session的钝化:
* 在服务器正常关闭之前,将session对象系列化到硬盘上
* session的活化:
* 在服务器启动后,将session文件转化为内存中的session对象即可。
* Session使用服务器上的Tomcat可以实现自动钝化和活化,但是使用IDEA不能实现,
把部署到Tomcat服务器上的Tomcat启动之后,将服务器关闭,tomcat会帮助我们在tomcat的work目录下,
持久化一个sessions.ser文件,记录我们上一次的操作,当再次重新启动tomcat之后,就会读取这个文件
还原关闭tomcat之前操作的信息。
3. session什么时候被销毁?
1. 服务器关闭
2. session对象调用invalidate() 也会被销毁
3. session默认失效时间 30分钟
选择性配置修改
<session-config>
<session-timeout>30</session-timeout>
</session-config>
5. session的特点
1. session用于存储一次会话的多次请求的数据,存在服务器端
2. session可以存储任意类型,任意大小的数据
6.session与Cookie的区别:
1. session存储数据在服务器端,Cookie在客户端
2. session没有数据大小限制,Cookie有
3. session数据安全,Cookie相对于不安全
【*】基本概念
java是面向对象语言,可以跨平台跨操作系统进行开发,实现跨操作系统的本质是因为java为各种操作系统都提供了虚拟机,而java是运行在虚拟机上的
二、类的初始化和实例化的区别
- 初始化:在项目启动的时候将类的基本信息和静态的内容,存在内存中,类初始化只执行一次(项目启动的时候类初始化,只是将类的一行一行的语句存放在内存,并不会执行)
- 实例化:创建类对象的过程,这个过程会将类的非静态方法和变量存放在堆中,这个过程可以有多次,每次类实例化都会在堆中开辟一块新的内存空间(new的时候类实例化)
三、类的生命周期
1.加载
(1)通过类的全限定名获取类的二进制流
(2)将二进制流表示的类的静态结构存储在方法区中
(3)生成这个类对应的Class对象,作为访问这个类的入口
2.连接
(1)验证
- 文件格式验证
- 元数据校验
- 字节码验证
- 符号引用验证
(2)准备
为类中的所有静态变量分配内存空间,并为其设置初始值(由于还没有产生对象,所以实例变量不在此过程范围内)
(3)解析
将常量池中的符号引用转化为直接引用,这个阶段可以在初始化之后再进行
关键字解析:符号引用和直接引用的区别
在编译的过程中,每个java文件都会编译成Class文件,但是编译的时候,虚拟机并不知道所引用类的地址,所以就用符号代替,而在解析这个阶段就是将符号引用转化为直接引用的过程。
例子(个人理解):医院出生的每个小孩在保育床上之后都可能被①②③进行编号(实际情况不知,毕竟女朋友都没有,哪来的孩子),只有在孩子被爸爸妈妈带回家之后,才会真正的拥有属于自己的名字,并出现在家庭的户口本上,从此孩子的名字就代表了这个孩子,而在这之前代表这个孩子的就是①②③编号。上述的①②③就代表符号引用,孩子的姓名就是直接引用。
3.初始化
(1)基本概念
在连接的准备阶段已经对类的静态内容附过一次初始值,而在初始化阶段则是按照程序员开发的逻辑进行初始化
【例子】
public static int a1 = 1;
public static int a2 = 2;
static{
a3 = 3;
}
在连接的准备阶段:a1、a2和a3赋值0,而在初始化阶段才复制为1、2、3
(2)何时初始化
- 为一个类型创建新的实例时(new、反射、序列化)
- 调用一个类的静态方法时或变量
- JVM启动包含main方法的类时
4.使用
使用阶段包含主动引用和被动引用
主动引用会引发类的初始化、而被动引用不会
- 主动引用就是上面说的:为一个类型创建新的实例时(new、反射、序列化)、调用一个类的静态方法时或变量、JVM启动包含main方法的类时
- 被动引用:引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化、定义类数组Student[]、引用类的常量
5.卸载
在类使用完之后,如果满足下面的情况,类就会被卸载
- java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了
四、双亲委托机制
JVM设计者,把连接阶段的通过类的全限定名获取类的二进制流的过程,放在虚拟机之外去执行,以便程序根据需要,自己决定如何获取类,实现这个动作的模块成为类的加载器
1、双亲委派机制的工作流程
- 每个加载器都会有自己的缓存,加载过后就会存入自己的缓存,等到下次需要的时候直接返回。首先类加载器首查询自己是否加载过该类,如果已经加载,直接返回
- 当类加载器的缓存中没有此类的时候,就会委托父类去查找,父类按照同样的策略,如果一直没有则一直向上查找到bootstrp ClassLoader.
- 当所有的父类都没有此类,再由当前的类去加载,然后放入自己的缓存中,以便下次需要的时候直接返回。
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。
五、四类8中数据类型
数据类型 | 关键字 | 内存占用 | 取值范围 |
字节型 | byte | 1个字节 | -127~128 |
短整型 | short | 2个字节 | -32768~32767 |
整型 | int(默认) | 4个字节 | -231次方~2的31次方-1 |
长整型 | long | 8个字节 | -2的63次方~2的63次方-1 |
单精度浮点数 | float | 4个字节 | 1.4013E-45~3.4028E+38 |
双精度浮点数 | double(默认) | 8个字节数 | 4.9E-324~1.7977E+308 |
字符型 | char | 2个字节 | 0-65535 |
布尔类型 | boolean | 1个字节 | true,false |
六、类型转换
- 转换规则
范围小的类型向范围大的类型提升, byte、short、char 运算时直接提升为 int 。
byte、short、char‐‐>int‐‐>long‐‐>float‐‐>double |
- 强制转换
字节数大的不能自动转换为字节数小的,会损失精度
long像八升的水壶,int像四升水壶,long转换成int就像8升水,导入4升的壶,肯定会报错的
- String转换成int
int a = Integer.parseInt(str);
int a = Integer.valueOf(str).intValue();
- int转换成String
String str = String.valueOf(a);
String str = a.toString();
七、流程控制语句
- 判断语句
if(){
...
}else if(){
...
}
- 三元运算符
布尔表达式?布尔表达式为真:布尔表达式为假
八、选择语句
switch(表达式) {
case 常量值1:
语句体1;
break;
case 常量值2:
语句体2;
break;
...
default:
语句体n+1;
break;
}
- case具有穿透性
由于没有break语句,程序会一直向后走,不会在判断case,也不会理会break,直接运行完整体switch。
九、循环语句
- for
for(初始化表达式①; 布尔表达式②; 步进表达式④){
循环体③
}
- while
while(布尔表达式){
布尔表达式为真,执行方法体
}
- do while
do{
执行方法体
}while(布尔表达式)
- while和do while区别
while只有表达式为真的时候,才会执行方法体,do while最少执行一次方法体布尔表达式为真的时候,会执行第二次
- 跳出语句break和continue
①break
使用在循环语句和switch中,可以终止switch或循环
②continue
结束本次循环,继续下一次循环
十、方法重载和方法重写
(1)方法重载
- 同一个类中可以有多个同名的方法
- 参数列表不同即可,与返回值类型和修饰符无关
- 参数列表不同:参数个数不同,参数顺序不同,参数类型不同
(2)方法重写
- 子类的方法和父类的方法相同(返回值类型、方法名、参数完全相同)
- 子类重写父类的方法,不能降低父类的访问权限
(3)两者区别:方法重载是编译期多态,在编译的时候就确定了运行那个方法;重写:运行的时候才确定调用哪个方法
十一、数组
- 三种方式
(1)int[] arr = new int[3];
(2)int[] arr = new int[3]{1,2,3}
(3)int[] arr = {1,2,3};
- 数组的使用int[] arr = new int[]{1,2,3,4,5};
- 数组的长度是arr.length
-
数组的索引是从0开始,数组的最后一个值是length-1:arr[0]是1,arr.length-1是5
-
数组常见的操作
(1)获取数组中最大的值
int[] arr = {1,200,15,2000,40000};
int max = arr[0];
for(int i = 1; i < arr.length; i ++){
if(arr[i] > max){
max = arr[i]
}
}
System.out.println(max);
(2)数组反转
int[] arr = {1,15,7,500,300}
for(int min = 0, max = arr.length; min <= max; min++,max --){
int temp = arr[min];
arr[min] = arr[max];
arr[max] = temp;
}
十二、Random类
随机生成0-10的随机数
Random r = new Random();
int number = r.nextInt(10);
十三、super和this
- super三种用法
- 在子类的成员方法中,访问父类的成员变量 super.name,调用父类的name属性
- 在子类的成员方法中,访问父类的成员方法 super.method(),调用父类的method()方法
- 在子类的构造方法中,访问父类的构造方法 super(),子类的无参构造方法,默认调用父类的无参构造方法
public class Fu {
public String name = "父类的name属性";
public void method(){
System.out.println("这是父类的方法");
}
public Fu(){
System.out.println("这是父类的构造方法");
}
}
class Son extends Fu{
public String name = "子类的name属性";
public void test(){
System.out.println(super.name);
super.method();
}
public Son(){
}
public static void main(String[] args) {
Son s = new Son();
s.test();
}
}
输出结果
这是父类的构造方法
父类的name属性
这是父类的方法
- this三种用法
- 在本类的成员方法中,访问本类的成员变量
- 在本类的成员方法中,调用另一个成员方法
- 在本类的构造方法中,调用本类的另一个构造方法
- 注意事项
- 构造方法中使用super()和this()时,注意,在构造方法中只能使用一次,且必须在第一行
- 子类构造方法,会默认调用父类的无参构造方法super(),但是在子类的构造方法中,使用this(...)调用其他的构造方法时,就不会调用默认的super()
十四、java的四个特征
封装、继承、多态、抽象
- 封装
把属性和方法结合在一起就是封装
- 继承
通过extends子类可以拥有父类的非private属性和方法,并且可以对父类的方法进行重写,继承是类在功能上的扩展,继承是单继承,但是可以多重继承,但是继承使得代码的耦合度变大,使得代码的独立性变差
- 多态
多态分为两种方式,分别为运行时多态和编译时多态
(1)方法重载就是编译时多态,在代码编译的时候就已经确定好,要执行哪个方法
(2)重写时运行时多态,只有当程序运行起来才能知道,到底要调用哪个子类
- 抽象
抽象是对一类事务的描述,并不是对具体对象的描述,例如人类,这个描述并不包含某个真实的个体,而实对全体人类的一个统称
十五、抽象和接口
- 抽象类
①抽象类不能实例化
②抽象类中可以有非抽象方法,有抽象方法的类一定是抽象类
③抽象类中的方法只有方法名,没有方法体
④构造方法和静态方法不能声明为抽象方法
⑤抽象类的子类必须重写抽象类的抽象方法,给出具体的实现,除非子类也是抽象类
⑥抽象类中的内容不能用private修饰,因为抽象类中的内容就是要被子类具体实现的
- 接口
接口特性
①接口可以多重继承
②接口不能实例化
③接口中没有构造方法
④接口中的方法都是抽象方法
接口的变量的特性
①接口中的变量都是static或者final修饰的,默认是public static final修饰的,不能声明为public意外的访问权限
接口方法的特性
①接口中的方法默认是public abstract修饰的
-
抽象类和接口的区别
- 1. 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
- 2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
- 3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
- 4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
注:JDK 1.8 以后,接口里可以有静态方法和方法体了。
-
接口和抽象类在应用上的区别
接口和抽象类的概念不一样。这个可以理解为接口是对动作的抽象,抽象类是对本质的抽象
①抽象类表示的是:这个对象是什么。
②接口表示的是,这个对象能做什么。
例子:男人,女人,这两个类,他们的抽象类是人。说明,他们都是人。人可以吃东西,动物也可以吃东西,你可以把“吃东西”定义成一个接口,因为不论是人还是动物吃东西是共性,用接口没问题
在Java语言上,一个类只能继承一个类(抽象类),类是对一类事物特征的描述,人和动物不是一类事物,但是具有相同的动作,所以描述人或者动物可以使用抽象,抽象类也是类
总结:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
十六、static
- static修饰变量
- static生命的变量称为类变量
- static修饰的方法或变量在类加载的时候就已经初始化,被改类的所有实例共享,只有一份
- 可以使用对象.类属性调用,不过一般用类名.类属性调用
- static变量存在于方法区中
- static修饰方法
- 通过类名.调用
- static修饰的方法,不能调用非static修饰的方法,因为static修饰的方法属于类的,不需要通过类的对象调用,static修饰的方法优先于类的构造方法,所以静态方法不能调用非静态方法,static方法加载完成的时候,普通方法还没有加载完成
- static方法不能引入supper和this关键字,因为this代表的是调用这个函数的对象的引用,而静态方法是属于类的,不属于对象,静态方法成功加载后,对象还不一定存在 (super类似)
- 静态初始化块
- 如果希望类加载后就对整个类进行某些初始化的操作,可以使用static初始化块
- 类第一次加载时先执行静态块,类多次载入静态块只执行一次,static块经常用于static变量的初始化
- 在类的初始化时执行,不是在创建对象的时候执行
-
静态初始化块中不能访问非static成员
【*】其他面试题
1.final、finally、finalize的区别:
首先三者只是长相相似,实则没有任何关系
(1)final:修饰的属性值不可以被改变,修饰方法不可以被重写,修饰类不可以被继承
(2)finally:是异常处理语句的一部分,不论是否有异常都会执行finally的内容,例如IO流的关闭
(3)finalize: finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。(处理遗言的)
2.&和&&区别
&:两边都会执行,结果同时true的时候返回true,无论怎么样,两边的都会执行
&&:当左边的表达式的为False的时候,不会在执行右面的表达式,直接返回false;左边表达式为true再判断右边表达式,左右都为true返回true.
3.hashMMap和hashTable区别:
hashMap的键值都可以为空,线程不安全,hashTable的键值不能为空,线程安全,hashTable的线程安全实在修改数据的时候所著整个hashTable,效率低。
4.堆、栈、静态区
Java虚拟机的内存可以分为三个区域:栈stack、堆heap、方法区method area
(1)栈特点
是连续的内存空间,栈的存储结构就像子弹夹一样,先进后出,后进先出
是方法执行的内存模型,方法执行相关的调用都在这个栈里,每个方法被调用都会创建一个栈帧,存储局部变量,操作数和方法出口等;
JVM会为每一个线程创建一个栈,用于存放线程执行方法的信息(实际参数、局部变量等);
栈属于线程私有的,不能实现线程共享;
栈是由系统自动分配的,速度快;
(1.1)存放方法的参数值,局部变量的值等(主要存放基本数据类型和对象的引用)
(2)堆:JVM只有一个堆,是不连续的内存空间,分配灵活,速度慢,可以被所有的线程共享
(3)JVM中只有一个方法区,被所有线程共享!方法区其实也是堆,用来存储类、常量相关的信息(用来存储程序中永远不变,或者唯一的内容,类信息,class对象,静态变量,字符串常量等)
5.Java中的对象
java中有两种对象,分别是实例对象和Class对象,Class对象存放的就是类的运行时的信息,实例对象就是根据Class对象创建出来的
6.
当创建一个类的时候,JVM就会编译成Class对象,存放再同名的.class文件中,
运行时,jvm会检查这个类是否已经装配到内存中,如果没有会根据.class文件装配到内存中,
若是装载则根据.class文件生成class对象
7.servlet运行时调用的方法
(1)servlet执行时,首先要编译成.class文件,有jvm的类加载器加载到运行时容器,例如tomcat
(2)当有人访问servlet的时候,先运行自己的构造方法,然后调用init方法初始化
(3)之后运行service方法,在service方法中,根据前端表单的url时get还是post,调用doGet或者doPost方法
(4)当web容器关闭时,调用Destory方法销毁servlet对象。
10.数据库的字段如果是空的话(不是null),在通过js输出在页面上的时候,如果不做非空判断就会现null
10. ==、===
(1)== :当左右两端操作数类型不同的时候先转化为相同的类型在判断操作数
(2)===:两端操作数的类型不同直接返回false.
11.超链接和重定向都是两次请求
(1)第一次客户端向服务器端请求要请求的地址
(2)服务器端返回允许请求的地址
(3)请求上一步服务器端返回的地址
(4)服务器端返回数据
13.jsp的本质就是servlet
发现PrintWriter out = resp.getWriter();
out.print("<b>内容<b>");
out.flush();
这样写太麻了,才研发的jsp
14.重定向的/表示服务器的根目录。
15.Ajax
ajax发送异步请求的时候,其实就是浏览器在后台new 了一个子线程,去发送请求,主线程保持不变,异步请求就是个子线程,请求得到的数据,根据脚本对主线程进行修改。
16.mysql行转列,
SELECT userid,
SUM(CASE `subject` WHEN '语文' THEN score ELSE 0 END) as '语文',
SUM(CASE `subject` WHEN '数学' THEN score ELSE 0 END) as '数学',
SUM(CASE `subject` WHEN '英语' THEN score ELSE 0 END) as '英语',
SUM(CASE `subject` WHEN '政治' THEN score ELSE 0 END) as '政治'
FROM tb_score
GROUP BY userid
列转行
SELECT userid,'语文' AS course,cn_score AS score FROM tb_score1
UNION ALL
SELECT userid,'数学' AS course,math_score AS score FROM tb_score1
UNION ALL
SELECT userid,'英语' AS course,en_score AS score FROM tb_score1
UNION ALL
SELECT userid,'政治' AS course,po_score AS score FROM tb_score1
ORDER BY userid
【*】
web.xml中配置了url-pattern 为*.do,在Controller中,在使用@RequestMapping的时候请求后面加.do,和不加.do都一样,当时浏览器或者jsp界面的请求必须加上.do,否则根本进不去DispatcherServlet前端控制器。
【*】
声明:纯属个人理解,如果有误希望指出
解决表单重复提交的办法再网络上有很多,再次不赘述,只谈表单重复提交的原因。
1.重复提交的本质大家都明白,就是请求重复执行罢了,刷新界面就是请求不断重复的过程
2.那么对我们开发造成影响的重复提交是什么呢,当然不是查询啦,只要没人修改,查询多少次的结果都是一样的
3.当时当请求是添加、删除操作的,导致的结果就是只要刷新界面就会重复添加,重复的删除,这样就对导致我们只想添加一条数据或者只想删除一条数据,变成了添加多条删除多条。
4.重点来了,表单重复提交的几种情况
(1)目标界面即当前界面(也就是再A界面的操作之后目标界面仍然是A界面)的非查询动作的请求,此时刷新界面时必定会造成重复提交的问题,因为默认的是请求转发,地址栏的地址不会发生改变,请求仍然存在地址栏中,当刷新界面时就相当于重新执行了刚才的请求。
(2)目标界面不是当前界面,跳转到另一个界面之后,此时浏览器的地址栏发生变化,刷新界面不会造成重复请求,但是当网速比较慢的时候,处理的请求不够快,此时点击刷新按钮不会造成重复提交,当你刷新浏览器界面时会返现一个现象,请求
【*】
1.使用hibernate时POJO必须要实现无参构造方法
2.pojo推荐实现serializable序列化,推荐实现hashcode和equals方法
2.1原因:hibernate经常使用set集合来保存对象,而set集合
【*】
Hession:
(1)Hessian 是一个基于 binary-RPC 实现的远程通讯 library。
(2)使用二进制传输数据。Hessian通常通过Web应用来提供服务,通过接口暴露。
(2)Servlet和Spring的DispatcherServlet都可以把请求转发给Hessian服务。
(3)两种提供方式为:com.caucho.hessian.server.HessianServlet、org.springframework.web.servlet.DispatcherServlet。
【*】
1.JPA注解
(1)@Entity 表明该类 (UserEntity) 为一个实体类
(2)@Table(name = "数据库表明") 当实体类与其映射的数据库表名不同名时需要使用 ,该标注与 @Entity 注解并列使用,置于实体类声明语
(3)@Id:代表主键
(4)几对几主要看当前的实体,如果当前实体Students,实体中的对象为Class,则关系为多个学生对应一个班级为多对一
@ManyToMany(多对多)
@JoinColumn :与操作关联的数据库字段
(5)
@Id:表示当前字段为主键
@GenericGenerator(name = "idGenerator", strategy = "uuid"):自定义主键生成器
@GeneratedValue(generator = "idGenerator"):为实体生成一个唯一标识的主键
@Entity
@Table(name = "pe_fee_detail")
public class PeFeeDetail extends AbstractBean {
// Fields
@Id
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Fetch(FetchMode.JOIN)
@ManyToOne
@JoinColumn(name = "FK_CLASS_ID")
private PeTrainingClass peTrainingClass;
【*】
1.查询Sql并返回
//GeneralDao 为已经注册到spring中的通用操作数据库的类
@Resource(name = "core_generalDao")
private GeneralDao generalDao;
@Override
public User findByLoginIdAndSiteCode(String loginId, String siteCode) {
User user = null;
Map<String, Object> queryParams = new HashMap();
StringBuffer sql = new StringBuffer();
sql.append("SELECT u.id userId, ");
sql.append(" u.login_id loginId, ");
sql.append(" u.password password, ");
sql.append(" u.true_name trueName, ");
sql.append(" u.MOBILEPHONE mobilePhone ");
sql.append("FROM sso_user u ");
sql.append(" WHERE u.login_id = :loginId ");
sql.append(" AND u.site_code = :siteCode ");
queryParams.put("siteCode", siteCode);
queryParams.put("loginId", loginId);
List<Map> list = this.generalDao.getMapListBySQL(sql.toString(), queryParams);
if (list != null && list.size() > 0) {
Map map = (Map) list.get(0);
if (map != null) {
user = new User();
user.setId((String) map.get("userId"));
user.setLoginId((String) map.get("loginId"));
user.setPassword((String) map.get("password"));
user.setTrueName((String) map.get("trueName"));
user.setMobilePhone((String) map.get("mobilePhone"));
}
}
return user;
}
【*】
1.sql优化方式
- 创建索引
- 避免select * 查询业务不相关的字段
- 避免导致索引失效的可能性
- 分区分表
- 读写分离
- 使用存储过程
2.导致索引失效的情况
- where条件中加索引的varchar类型的字段要加引号,不加引号会导致索引失效
- 对索引字段进行算术运算会导致索引失效
- 创建索引之后不分析表结构可能会导致索引失效 analyze table student compute statistics
- 使用<>、not in、not exists、
- like '%-‘
3.分区分表
- 分表
就是将一张大表分成小表 - 分区
将一张表的数据分成多个区
【*】having常和group by 和聚合函数连用
高快省的排序算法
有没有既不浪费空间又可以快一点的排序算法呢?那就是“快速排序”啦!光听这个名字是不是就觉得很高端呢。
假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了)。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列:
3 1 2 5 4 6 9 7 10 8
在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。想一想,你有办法可以做到这点吗?
排序算法显神威
方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。
首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:
6 1 2 5 9 3 4 7 10 8
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:
6 1 2 5 4 3 9 7 10 8
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:
3 1 2 5 4 6 9 7 10 8
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。
左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。好了开始动笔吧
如果你模拟的没有错,调整完毕之后的序列的顺序应该是:
2 1 3 5 4
OK,现在3已经归位。接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下:
1 2 3 4 5 6 9 7 10 8
对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下
1 2 3 4 5 6 7 8 9 10
到此,排序完全结束。细心的同学可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。下面上个霸气的图来描述下整个算法的处理过程。
这是为什么呢?
快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫做“二分”的思想。我们后面还会遇到“二分”思想,到时候再聊。
【*】
1.spring的单例bean和多例bean
单例bean:IOC容器初始化的时候创建bean对象,每次调用的时候直接从容器(MAP)中直接获取,不需要再次创建
多例bean:IOC容器初始化的时候不创建bean对象,当需要bean对象的时候再创建。
验证单例和多利区别可以从AnnotationConfigApplicationContext中获取bean,例如
Object bean1 = app.getBean("");
Object bean2 = app.getBean("");
单例bean1 == bean2 返回值是true
多例bean1 == bean2 返回值是false
2.懒加载
懒加载主要针对单实例bean,IOC容器初始化的时候不创建bean对象,当需要的时候创建,
创建一次之后,再获取的时候直接获取已经拆个年间的这个bean
3.FactoryBean和BeanFactory区别
FactoryBean:把java实例bean通过FactoryBean注入到容器中
BeanFactory:从容器中获取实例后的bean
4.多实例,IOC容器只负责初始化(getBean的时候才会初始化)不会管理bean,容器关闭的时候不会调用销毁方法
5.单例的BeanFactory是将bean放在了缓存中,销毁的首先在缓存中销毁(将Map中的内容,通过clear()清空)
6.AnnotationConfigApplicationContext是spring加载配置类的入口
7.Bean的生命周期: 创建-----初始化----销毁
7.1 自定义bean的初始化方法有三种方式:容器在 bean 进行到当前生命周期的时候, 来调用自定义的初始化和销毁方法
(1)指定初始化和销毁方法,配置文件方式: <之前在 beanx.xml, 可以指定 init-method 和 destory-mothod>;
注解方式:在配置类里通过@Bean(initMethod="init", destroyMethod="destroy")指定,单实例: 当容器关闭的时候,会调用 destroy
(2)让 Bean 实现 InitializingBean 和 DisposableBean 接口
(3)使用 JSR250 规则定义的(java 规范)两个注解来实现 @PostConstruct @PreDestroy
7.2 Bean的前置后置处理器
自定义类实现BeanPostProcessor接口,重写postProcessBeforeInitialization和postProcessAfterInitialization方法
(1)postProcessBeforeInitialization在bean初始化之前执行
(2)postProcessAfterInitialization在bean初始化完成之后执行
8.@Autowired和@Qualifier
@Autowired:可以将一个类的实例注入到另一个类中
@Qualifier("bean的ID"):当一个类有多个相同类型的bean时,使用此注解可以指定注入那个bean,
当使用,@Qualifier时,@Autowired的byType注入方式就变成了byName方式
9.@Resource和@Autowired
(1)两者效果是一样的,@Autowired是Spring的,@Resource是JDK的JSR250规范的实现,需要导入javax.annotation实现注入。
(2)@Autowired是按照类型(.Class)进行注入。
(3)@Resource如果不指定name和type,默认按照名称(bean的ID)进行注入
指定name按byName注入,找不到报错
指定type按照byType注入,找不到报错
(4)@Resource和@Primary同时使用时,如果@Resource没有指定按name或者type匹配则@Primary会生效,其他情况优先级不生效
(5)@Autowired(required=false):当要注入的实例不存在,这样写不会报错,推荐使用@Autowired方式
10.@Primary
此注解代表优先级比较高
11.@Inject
(1)是JSR330中的规范,需要导入javax.inject.Inject;实现注入。
(2) 根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Named;
(3)可以作用在变量、setter方法、构造函数上。
12.Spring中的Aware接口作用:获取spring容器的服务
13.AOP切面通知
1.注解
(1)@Aspect:标注在类上,表示这是一个切面类,供容器读取
(2)@Pointcut:标注在方法上,在空方法上写此注解,切他的切面注解在变量里可以直接引用此方法的方法名,减少冗余(避免每个注解后面都写execution)
execution(* *(..)):表示匹配所有的方法
execution(public * com.savage.service.UserService.*(..)):表示com.savage.service.UserService类中的所有 访问修饰符为public 返回值任意的 方法
execution(* com.savage.server.*.*(..)):表示com.savage.server包下所有类的所有方法
execution(* com.savage.server..*.*(..)):表示com.savage.server下所有子包下所有类的所有方法
【例】
@Pointcut("execution(public int com.lby.aop..*.*(..))")
public void pointCut(){}
//@Before的参数直接写pointCut()即可,不需要重新写execution(public int com.lby.aop..*.*(..))
@Before("pointCut()")
(3)@Around:环绕增强,比@Before还要先执行
【例】
public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("@Arount:执行目标方法之前...");
Object obj = proceedingJoinPoint.proceed();//相当于开始调用目标方法(反射机制调用)
System.out.println("@Arount:执行目标方法之后...");
return obj;
}
(4)@Before:标识一个前置增强方法,在目标方法之前执行
(5)@After: 标识一个结束方法,在目标方法执行结束执行
(6)@AfterReturning:后置返回通知,目标方法执行正常退出时执行,出现异常不执行
(7)@AfterThrowing:异常抛出增强,目标方法抛出异常执行
2.对方法进行切面处理步骤
(1)引入spring-aspects坐标
(2)创建要进行切面处理的的类
(3)创建切面类
切面类上要写@Aspect,切面类里面写的时切面的方法
(4)创建配置类
在配置类上面加@Configuration注解,代表此类时配置类。
在配置类上面加@EnableAspectJAutoProxy,打开AOP的开关,可以使用AOP的切面功能
将切面类和被切面处理的类通过@Bean或者其他方式注入到容器中
(5)创建测试类
将配置类加载到IOC容器:ApplicationContext app = new AnnotationConfigApplicationContext(Config.class);
调用被进行切面处理的方法,查看是否被切面处理(此处调用切面方法,通过getBean()方式,才能被切面处理,如果直接实例化,没有效果)
3.JoinPoint:
在切面方法中定义JoinPoint类型的参数,可以获得目标方法的方法名和参数列表
(1)JoinPoint.getSignature().getName():获取方法名
(2)Arrays.asList(JoinPoint.getArgs()):获取参数列表
4.@AfterReturning(value = "pointCut()",returning = "a")
在后置返回通知方法中,添加returning = "a",在后置反回方法的参数中定义Object类型的参数接收returning的值,可以将执行结果输出
【例】
@AfterReturning(value = "pointCut()",returning = "a")
public void logReturn(Object a){
System.out.println("反回通知"+a);//a的值返回的时被处理方法的执行结果
}
5.异常通知:throwing的值和方法的参数名相同,当被处理方法产生异常时,此操作可以返回异常
【例】
@AfterThrowing(value="pointCut()",throwing="exception")
public void logException(Exception exception){
System.out.println("异常通知:"+exception);
}
14.事务
1.编程式事务:由程序员编程事务控制代码.
2.声明式事务:事务控制代码已经【由 spring 写好】.程序员只需要声明出【哪些方法需要进行事务控制】和【如何进行事务控制】.
3.声明式事务都是针对于 ServiceImpl 类下方法的.
4.事务管理器基于通知(advice)的.
5.在 spring 配置文件中配置声明式事务
6.spring的事务和AOP的关系,spring的声明式事务,在本质上和AOP不是一个概念,只是用了AOP的方式
(1)AOP通过bean将SleepHelperAspect切面类注入到容器中,在配置文件中定义切点(进行切面的范围),
在aspect标签中引用切面类,在通知方法中引用切点,这样就完成了切面
(2)声明式事务
将事务管理器(DataSourceTransactionManager针对数据源的数据管理器)注册到容器中。
<tx:advice>配置声明式事务,将事务管管理器传入,定义那些方法需要被事务管理,支持通配符
<aop:pointcut>定义切点。
<aop:advisor>引用切点,引用<tx:advice>
(3)AOP和声明式事务,在实现方式上,声明式事务比AOP多了一个<tx:advice>过程。
【例】AOP
//切面类
public class SleepHelperAspect{
public void beforeSleep(){
System.out.println("睡觉前要脱衣服!");
}
public void afterSleep(){
System.out.println("起床后要穿衣服!");
}
}
//AOP配置
<bean id="sleepHelperAspect" class="com.ghs.aop.SleepHelperAspect"></bean>
<aop:config>
<aop:pointcut expression="execution(* *.sleep(..))" id="sleepPointcut"/>
<aop:aspect ref="sleepHelperAspect">
<!--前置通知-->
<aop:before method="beforeSleep" pointcut-ref="sleepPointcut"/>
<!--后置通知-->
<aop:after method="afterSleep" pointcut-ref="sleepPointcut"/>
</aop:aspect>
</aop:config>
【声明式事务】
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="ins*" />
<tx:method name="del*"/>
<tx:method name="upd*"/>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut expression="execution(* com.bjsxt.service.impl.*.*(..))" id="mypoint" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="mypoint" />
</aop:config>
(4)
<!-- 扫描器 -->
<!--
1.当加入<context:property-placeholder>标签后,MapperScannerConfigurer不能使用自动注入的方式
2.SqlSessionFactoryBean的bean的id名不能为sqlSessionFactory,也就是不可以和MapperScannerConfigurer的bean标签的property属性的name相同
3.MapperScannerConfigurer的bean标签的下面property的name不能用SqlSessionFactory
(不用.properties属性配置文件时MapperScannerConfigurer的name和SqlSessionFactoryBean的bean标签的id相同可以实现自动注入,MapperScannerConfigurer的第二个property可以不用配置)
只能使用sqlSessionFactoryBeanName才会配置生效,且不能和SqlSessionFactoryBean的bean标签的id相同
-->
<bean id="factory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="typeAliasesPackage" value="com.bjsxt.pojo"></property>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.bjsxt.mapper"></property>
<property name="sqlSessionFactoryBeanName" value="factory"></property>
</bean>
【*】什么是HTTP协议
客户端与服务器端进行传输数据,简称超文本传输协议,本质是基于socket tcp协议,支持html,json,text,流。到那时最终都是基于二进制传输。
请求的过程
客户端,通过request请求服务器端,然后服务器端将结果相应(response)给客户端
- 服务器端作用:(例如tomcat,webLogic 服务器端作用就是将本地的数据共享给外部的资源)
- 客户端:谷歌,360
HTTP无状态协议
客户端向服务器端发送请求没有事务的管理机制,就算发送请求的过程当中客户端和服务器端断开了,也不会发生任何回滚。
socket层通讯
socket客户端向socket服务器端发送请求,服务器端需要提供IP和端口号,而且是tcp协议,需要三次握手和四次释放,本质是通过二进制流进行传出的,所以socket通讯和使用的语言(java,c)没有任何的关系
HTTP格式的分类
- 请求(HttpServletRequest对象)
(1)请求行
(2)请求头
(3)请求内容
(4)请求体(只有在post请求中才会有) - 相应(HttpServletResponse对象)
(1)相应行
(2)相应内容
(3)相应体(html查可能到的元素)
请求行
- 请求地址
Request URL:http://www.mayikt.com/static/imgages/index-img/icon5@2x(1).png -
请求方式(http RESTfu风格 psot delete put)
Request Method:GET
请求头
- Accept: text/html,image/* -- 浏览器接收数据类型
- Accept-Charset: ISO-8859-1 -- 浏览器接收的编码格式
- Accept-Encoding: gzip,compress --浏览器接收的数据压缩格式
- Accept-Language: en-us,zh- --请求的编码
- Host: www.it315.org:80 --请求域名的地址
- If-Modified-Since: Tue, 11 Jul 2000 18:23:51 GMT --浏览器最后的缓存时间
- Proxy-Connection: keep-alive --代理类型
- Referer: http://www.it315.org/index.jsp -- 当前请求来自于哪里
- User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0) --浏览器版本类型
- Cookie:name=eric -- 浏览器保存的cookie信息
- Connection: close/Keep-Alive -- 浏览器跟服务器连接状态。close: 连接关闭 keep-alive:保存连接。
- Date: Tue, 11 Jul 2000 18:23:51 GMT -- 请求发出的时间
很多互联网公司在url后面加时间戳
【小知识】http状态码304表示从本地请求的(缓存 ),不是在服务器上取的
(1)在静态资源(css/img/js等)后面加上时间戳的作用就是可以防止浏览器读取缓存,在版本更新的时候由于用户的浏览器会有缓存,所所以在发布版本的时候用户看到的界面可能是之前缓存的图片和资源,在静态资源后面加上时间戳之后,在发布版本,用户的浏览器就会读取最新的图片等静态资源。主要解决版本代码更新不同不的问题。
(2)浏览器的机制会判断例如图片的地址是否发生变化,没有变化就会读取本地缓存的的,有变化就会读取服务器的,所以加上时间戳就会避免缓存的问题。
防盗链
当A网站的链接资源,被B网站获得,此时B网站可以频繁的刷新这个从A网站获取的链接,这样就会频繁的访问A网站的链接,导致浪费了A网站的宽带
防止办法
开发者工具可以查看请求链接的来源,比如本地域名是www.lby.com,图片是lby.png,所以图片的来源,应该是www.lby.com后面加上图片的路径,但是如果请求当前图片资源的连接来源是其他的域名,则说明是非法盗用本地的链接。(查看来源位置如下图)
所以解决的思路就是通过获取Referer获取后面的请求中的域名和本地的域名进行比对,如果匹配上了则是正常请求,否则是非法请求。
请求转发和重定向,转发一般跳转内部服务器,重定向一般跳转外部服务器。
重定向,客户端访问服务器,服务器会在响应头中返回一个302的状态码,然后客户端就会去找location的值,服务器端就会帮助客户端跳转location中值的位置。
HTTP和HTTPS
企业中的黑名单白名单其实就是判断请求头的来源
WebSocket
- http协议无状态和单向性
- 无状态:浏览器向服务器第一次发送请求服务器响应,浏览器第二次向服务器发送请求,此时服务器认为这是一次全新的请求,服务器不知道发送这个请求的人是上一个用户,http为了解决这样的无状态的问题,提出了cookie和session
- 单向性:只能由浏览器发送请求,服务器接受,服务器不能主动的向浏览器发送内容,是一请求一响应的方式,就算网页上想要不通过设置动态的显示服务器的时间都不容易。需要用户去手动刷新
- 基于http的解决方案:
- ajax短轮循,js不停的定时的向服务器去发送请求,服务器进行应答,缺点是浪费带宽,响应结果有延迟,
- 由此产生了ajax长轮循,相比较短轮循,长轮询在服务器有新内容更新的时候才会响应请求。
- html5又出现了SSE和webSocket
- SSE和长轮训相比,当服务器响应给客户端数据之后,让客户端别断开连接,我可能有新的消息要再返回给你,但是SSE最大的缺点就是只支持文本
- WebSocket建立了一次连接以后,服务器端就和客户端一直保持这一个连接
- 实现了客户端和服务器端的双向通讯
- 基于文本和二进制传输协议,因此可以支持图片等资源的传输,这是SSE代替不了的
- 并不是所有的浏览器都支持,对于版本比较旧的版本不支持
- WebSocket借用HTTP一部分来完成握手的
- WebSocket和TCP的三次握手四次挥手还是有区别的,是建立再这个基础之上的,在tcp建立完成之后发生的
会话
Session
图中的JSESSIONID就是创建session的ID(设置的时候在response里面将值相应给浏览器)
获取的时候在请求头(Request)中获取
session的值是存放在服务器的内存当中
session原理图
创建:session的值是存放在服务器的内存当中,session创建之后会将sessionID通过请求头的方式返回给客户端保存
获取:客户端将本地的sessionID传递给服务器端,通过sessionID查找session的值
关闭浏览器SessionId会失效。但是session的值是存在服务器端的,失效的值默认是30分钟,所以关闭浏览器session并不会立即失效。
服务器停掉session值会立即失效,再次访问浏览器是获取不到的(已验证)
Cookie
什么是会话
会话就是信息交流,会产产生信息
会话产生的场景
- 一次会话就是用户打开浏览器->访问浏览器的资源->关闭浏览器
- (场景一)在浏览器在爱奇艺官网的登录账号->关闭浏览器->再次登录浏览器发现不需要再次登录
实现会话的技术
- Cookie 会话产生的数据会保存在浏览器客户端
- Session 产生的数据会保存在服务器端
会话管理
浏览器(客户端)和服务器之间会话的过程会产生会话数据
Cookie
什么是cookie
在客户端本地存储一些数据,当客户端访问浏览器,客户端的一些信息,例如登录信息就会保存在本地,当下次再次访问的时候可以无需再次登录
使用cookie的注意事项
- 不同浏览器的cookie存放位置不同,所以谷歌浏览器上的cookie信息在其他浏览器上是获取不到的,cookie是不能跨浏览器的
- cookie默认浏览器关闭就失效,但是可以设置cookie失效时间,这样关闭浏览器cookie也不会立即失效
- 而且cookie存放的位置和域名有关,他会根据域名去保存到本地,所以当域名更换之后cookie就找不到了(例如,存储cookie的时候使用的是127.0.0.1但是访问的时候使用localhost这样同样是不可以的)
-
Cookie数据类型只能保存非中文字符串类型的。可以保存多个cookie,但是浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie的大小限制为4KB。
Cookie生命周期
当客户端首次登录浏览器时,服务器端会创建cookie,并且将cookie的信息放在响应头中,返回给客户端;客户端获取响应头中的cookie信息,并存储在客户端的本地。当客户端再次访问网站时,客户端会将本地存放的cookie信息,通过请求头的方式,将cookie信息发送给服务器
【*】什么是幂等性
首先幂等性是数学中的概念:一个函数执行多次,返回结果相同就达到了幂等性的要求 f(f(x)) = f(x)
【*】软件开发中的幂等性
- 什么情况下会产生幂等性
在业务上对某一事件进行多次处理,但是预期只想处理一次,例如支付过程中,购买一件商品,由于网络原因第一次没有支付成功,用户又刷新进行重复请求,此时用户的主观想法当然不是想要购买多次,此时如果发生了两次扣款,就产生了幂等性的问题。
- 在CURD过程当中的幂等性
- select和delete:查询天然支持幂等
- update:可能不幂等,也可能幂等
- 幂等:修改的最终值固定,例如进行状态修改,将可用的状态修改成不可用,进行多次修改,结果都是由可用变成不可用
- 不幂等:修改的最终值不固定,例如支付扣款,账户余额1000元,购买了200元的商品,余额就是1000-200,发生多次就变成了重复扣款
- insert:天然不幂等
- 多次添加必然会每次数据都会变多,必然结果发生了变化,所以不幂等
【*】幂等性问题怎样解决
- 唯一索引
新建一张去重表,里面可以只放一个唯一索引字段
例如订单幂等性:不能重复生成订单,订单号可以使用一些特定的规则进行生成(注意保证订生成单号的唯一性),新建一张表,只存储订单号,并将其设置为唯一索引,生成订单方法,在真正调用生成订单的业务逻辑之前,需要首先在订单去重表中,
- 状态标识
- 版本号
- token令牌
【*】序列化和反序列化
1.序列化
可以将对象转换成字节流从内存中取出,存到硬盘上
2.反序列化
将字节流转化为对象从硬盘取出,转化成对象
3.字节流是与平台无关的,所以一个平台上的字节流可以在不同平台上反序列化为对象,所以序列化可以实现跨平台或者在网络上传递。
4.实现序列化的方式
实现java.io.Serializable接口
5.如果序列化一个不可序列化的对象将会报NotSerializableException异常
【*】反射
1.什么是反射
在程序运行期间,动态加载一个只有类名或者其对象的未知类,我们只要有这个类的类名或者对象就可以调用这个类的属性和方法(包括私有的)。
1.1例如
Class c = Class.forName(类名);
1.2 加载类之后,就会产生一个Class类型的对象(一个类只有一个Class类型的对象),这个对象包含了完整的类的结构信息,我们通过这个对象就可以看到这个类的结构,就像一面镜子一样,这种现象我们称为反射
1.3 获取Class对象的三种方式:
1.3.1 getClass()
1.3.2 Class.forName()
1.3.3 .Class
1.4 反射获取类内容的方式
Class clazz = Class.forName(类的全路径);
//获取类的名字
clazz.getName()//获得包名+类名:com.lby.test.bean.User
clazz.getSimpleName() //获的类名:User
//获取属性信息
Field[] fields = clazz.getFields(); //只能获得public的field
Field[] fields = clazz.getDeclaredFields();//获得所有的field
Field f = clazz.getDeclaredField("uname");
f.setAccessible(true); //不需要执行访问安全检查 ,能够提高效率
//获取方法信息
Method[] methods = clazz.getDeclaredMethods();
Method m01 = clazz.getDeclaredMethod("getUname", null);
//如果方法有参,则必须传递参数类型对应的class对象,第二个参数为要通过反射获取方法的参数
Method m02 = clazz.getDeclaredMethod("setUname", String.class);
//获得构造器信息
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor c = clazz.getDeclaredConstructor(int.class,int.class,String.class);
//获得类的所有有效注解
Annotation[] annotations=clazz.getAnnotations();
//获得类的指定的注解
clazz.getAnnotation(LbyTable.class);
//获得类的属性的注解
Field f = clazz.getDeclaredField("studentName");
f.getAnnotation(SxtField.class);
//获得指定方法参数泛型信息
Method m = Demo04.class.getMethod("test01", Map.class,List.class);
Type[] t = m.getGenericParameterTypes();
//获得指定方法返回值泛型信息
Method m2 = Demo04.class.getMethod("test02", null);
Type returnType = m2.getGenericReturnType();