赞
踩
- 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
- 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
- }
-
- }
在equals中源码的注释上,有一句相同对象必须有相同哈希值,所以我们在开发的时候应该遵守这个约定,所以equals判断两个对象相等时,hashCode也应该相等
通过阅读HashMap的源码发现,在HashMap存储键值对(key,value)的时候,对象存存储在HashMap集合数组的下标的计算方式是通过HashCode计算的,所以hashCode的值会影响HashMap集合中存放对象的方式,
hashCode方法不仅仅是与equals配套使用的,它甚至是与Java集合配套使用的,为了避免代码出现问题,还是尽量按照规定办事儿
那么根据取余算法
数字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
存储一个key,value时,首先计算key的hash值,根据hash值和数组的长度,确定插入数组的位置
但是可能存在同一个hash值对应的数组位置已经有对象插入了
这时就添加到同一hash值元素的后面,他们在数组的同一位置,这样就形成了链表
jdk8中,当链表的长度大于8时,链表就转化为红黑树,这样有大大的提高了查找的效率
- 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);
- }
-
- }
map.get(key)
其中参数initialCapacity为初始容量,loadFactor为加载因子,扩容就是在put加入元素的个数超过initialCapacity * loadFactor的时候就会将内部Entry数组大小扩大至原来的2倍,然后将数组元素按照新的数组大小重新计算索引,放在新的数组中,同时修改每个节点的链表关系(主要是next和节点在链表中的位置)。所以数组扩容是非常耗费时间的
数组是一个连续的空间,查询比较快,但是添加删除速度比较慢
链表不是连续的空间,查询速度慢,但是添加和删除速度比较快
所以将两者的优点结合起来就是Hash表
key经过hash算法计算出的hash值,在数组中对用的位置已经有值了,这种情况被程为hash冲突,也就是说两个不同的对象的经过hashCode相同,这种情况就是hash冲突
hashMap解决hash冲突的办法就是链地址法,将计算出hash值相同的不同对象,通过链表的方式存放在数组里面
jdk7采用的链表头插法的方式,在多线程并发的情况下,会发生环形链表,或数据丢失的情况
jdk8采用尾插法,不会形成环形链表,但是会出现数据覆盖的情况,所以线程依然不安全
HashMap并不是线程安全的,支持K和V为null ,k重复会覆盖,V可以重复,还有一点HashMap遍历的数据不是有序的是无序的
扩展:final空白,就是在类中定义一个没有赋初始值的final的变量,但是前提是需要提供一个只有当前没有复制final修饰参数的构造方法,并且不能有其他的任何构造方法,目的就是根据对象的不同,final修饰的属性值不同
方法一:向上取整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;
String 不属于基础类型,基础类型有 8 种:byte、boolean、char、short、int、float、long、double,而 String 属于对象。
操作字符串的类有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象(String类是被final修饰的),每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
栈:
这就是出栈入栈的过程,入栈出栈的原则是先进后出,后进先出,就像枪打子弹一样,先放进去的子弹最后打出,最后放进区的子弹最先被打出
堆:
方法区
常量池
【注】常量池在JDK1.6的时候,放在方法区中。到了JDK1.7,转移到了堆区。到JDK8时取而代之的是元空间。
【注】类的方法只有一套,被所有类的对象所共享,只有使用方法的时候才会入栈,进行内存的分配,不使用方法不占用内存
年轻代
年老代
持久代
堆内存的划分细节:
[注]下列图示中的蓝色区域代需要被回收的对象占用的内存空间,黄色为存货对象占用的内存空间
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)
分代垃圾回收机制是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。Java虚拟机将对象分为三种状态:年轻代、年老代、持久代。JVM将堆内存划分为 Eden、Survivor 和 Tenured/Old 空间。
1、引用计数(Reference Counting)
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
2、复制(Copying)
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间,年轻代中的survivor区使用的就是复制算法
[注]橙色蓝色和白色代表未被使用的空间,灰色代表需要被回收的空间,绿色代表需要被复制的空间
3、标记-清除(Mark-Sweep)
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
就像一张由对象组成的关系网,从第一个创建并放在这张关系网上的对象开始,所有在这张网上有引用关系的对象都会被标记,没有被标记的对象说明没有被引用,那么这个没有被引用的对象就是需要被垃圾回收的对象,将需要被回收的对象占用的空间清除之后,会产生空间碎片,且此算法会暂停整个应用。
4、标记-整理(Mark-Compact)
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。简图如下:
jvm有7个垃圾收集器
- // 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
抽象类不一定需要抽象方法,但是有抽象方法的类一定是抽象类
不能,因为抽象类的目的就是要给其他类区继承,从而达到实现多态的目的,使用了final代表这个类不可以被继承,那么抽象类的目的就已经失效了。
在jdk8之前,interface之中可以定义变量和方法,变量必须是public、static、final的,方法必须是public、abstract的。
JDK8及以后,允许我们在接口中定义static方法和default方法
原因:如果接口中只能定义抽象方法,当修改接口时,该接口的所有实现类都会受到影响,为了减小这种影响,所以才采用了这种方式
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap
①List:元素不唯一,有序(索引顺序)
②Set:元素唯一,无序
③Map:存储一组键值对,提供key、value映射
HashSet
LinkedHashSet
采用哈希表存储结构,同时使用链表维护次序
有序(添加顺序)
TreeSet
采用二叉树(红黑树)的存储结构
优点:有序 查询速度比List快(按照内容查询)
缺点:查询速度没有HashSet快
Value 无序 不唯一 (Collection)
LinkedHashMap
有序的HashMap 速度快
TreeMap
有序 速度没有hash快
问题:Set和Map有关系吗
采用了相同的数据结构,只用于map的key存储数据,就是Set
所有集合类均未提供相应的遍历方法,而是把把遍历交给迭代器完成。迭代器为集合而生,专门实现集合遍历
(1)数据结构图解
①jdk1.7
②jdk1.8
图解
①hashMap在put的时候才会去初始化数组,类似于懒加载,只有在用到的时候才会进行初始化,HashMap中数组长度默认是16
②通过key计算Hash值,然后和数组的长度减一进行&(与)运算,得到数组的下标
③计算的数组下标中没有对象,直接将当前对象放进数组;有对象,将对象放在前一个对象的后边
④数组上链表的长度大于或者等于8,链表会转化位红黑树,提高了查找的效率
map.get(key)
(1)四种方式
- //1、分别获取key和value的集合
- for(String key : map.keySet()){
- System.out.println(key);
- }
- for(Object value : map.values()){
- System.out.println(value);
- }
- //2、获取key集合,然后遍历key,根据key得到 value
- Set<String> keySet = map.keySet();
- for(String str : keySet){
- System.out.println(str+"-"+map.get(str));
- }
- //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 都为 null。key 重复会被覆盖,value 允许重复。
②非线程安全
③无序(遍历HashMap得到元素的顺序不是按照插入的顺序)
①HashMap其实就是key/value的键值组合,每个键值组合就是一个Entry,这个Entry分散的存储在数组上
①首先HashMap并不是线程同步的
②在扩容的时候,链表的位置会发生反转,这样在高并发的情况下,非常容易链表环,引起死循环
③HashMap并不是线程安全,所以在多线程情况下,应该首先考虑用ConcurrentHashMap。避免悲剧的发生
④jdk7采用的链表头插法的方式,在多线程并发的情况下,会发生环形链表,或数据丢失的情况
⑤jdk8采用尾插法,不会形成环形链表,但是会出现数据覆盖的情况,所以线程依然不安全
①使用Hashtable替代HashMap,Hashtable内方法上使用了synchronized。
②ConcurrentHashMap,方法内部使用了synchronized保证线程安全。【*】推荐使用
③Collections 类的synchronizedMap(Map<K,V> m)方法可以返回一个线程安全的Map
④ConcurrentHashMap生成的Map性能是明显优于Hashtable和Collections 的synchronizedMap()方法。
①优化 hash 算法只进行一次位移操作
②引入红黑树,在冲突比较严重的情况下,将 get 操作的时间复杂从 O(n) 降为了 O(logn)
①当key的hash值和数组的长度减一进行与运算的时候,会得到相同的值,这就是hash冲突
①使用链地址法,将冲突的元素,追加在前面的元素后面,形成链表
作者 | ThinkWon
来源 | blog.csdn.net/ThinkWon/article/details/101681073
Java异常是Java提供的一种识别及响应错误的一致性机制。
Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。
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)。往期:一百期面试题汇总
try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
throw – 用于抛出异常。
throws – 用在方法签名中,用于声明该方法可能抛出的异常。
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 方法来获取。往期:一百期面试题汇总
Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复;
Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。
运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。Java 编译器不会检查运行时异常。
受检异常是Exception 中除 RuntimeException 及其子类之外的异常。Java 编译器会检查受检异常。
RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
throws 关键字和 throw 关键字在使用上的几点区别如下:
throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。
引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;
ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。往期:一百期面试题汇总
答:catch 可以省略
原因
更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。
理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。
至于加上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
有如下代码片断:
- 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!
ClassCastException(类转换异常)
IndexOutOfBoundsException(数组越界)
NullPointerException(空指针)
ArrayStoreException(数据存储异常,操作数组时类型不一致)
还有IO操作的BufferOverflowException异常
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或大于等于序列大小时,抛出该异常。
在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。
本文给出几个被很多团队使用的异常处理最佳实践。
当使用类似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);
- }
- }
你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异常。
因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的处理异常并且避免额外的检查。
因此,总是尝试寻找最适合你的异常事件的类,例如,抛出一个 NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。
- public void doNotDoThis() throws Exception {
- ...
- }
- public void doThis() throws NumberFormatException {
- ...
- }
当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。
在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。
- public void doSomething(String input) throws MyBusinessException {
- ...
- }
在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。
但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。
如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被 java.lang.Long 类的构造函数抛出。
- try {
- new Long("xyz");
- } catch (NumberFormatException e) {
- log.error(e);
- }
大多数 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)
- }
- }
Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做!
如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。
所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。
- public void doNotCatchThrowable() {
- try {
- // do something
- } catch (Throwable t) {
- // don't do this!
- }
- }
很多时候,开发者很有自信不会抛出异常,因此写了一个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);
- }
- }
这可能是本文中最常被忽略的最佳实践。可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下:
- 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);
- }
- }
因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。
捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。
在你这样做时,请确保将原始异常设置为原因(注:参考下方代码 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);
- }
- }
不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能。
如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。
异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。
仅在异常情况下使用异常;
在可恢复的异常情况下使用异常;
尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。往期:一百期面试题汇总
综上所述,当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 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) {…}
一次会话包含多次请求和响应,一次会话就是浏览器第一次向服务器资源发送请求,会话建立,知道其中一方断开为止,会话结束
功能:在一次会话的范围内的多次请求间,共享数据
方式:
1. 客户端会话技术:Cookie
2. 服务器端会话技术:Session
客户端会话技术,将数据保存到客户端
* 使用步骤:
- 1. 创建Cookie对象,绑定数据
- * new Cookie(String name, String value)
- 2. 发送Cookie对象
- * response.addCookie(Cookie cookie)
- 3. 获取Cookie,拿到数据
- * Cookie[] request.getCookies()
* 基于响应头set-cookie和请求头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可以共享
- 1. cookie存储数据在客户端浏览器
- 2. 浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)
- * 作用:
- 1. cookie一般用于存出少量的不太敏感的数据
- 2. 在不登录的情况下,完成服务器对客户端的身份识别
- 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");
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是运行在虚拟机上的
(1)通过类的全限定名获取类的二进制流
(2)将二进制流表示的类的静态结构存储在方法区中
(3)生成这个类对应的Class对象,作为访问这个类的入口
为类中的所有静态变量分配内存空间,并为其设置初始值(由于还没有产生对象,所以实例变量不在此过程范围内)
将常量池中的符号引用转化为直接引用,这个阶段可以在初始化之后再进行
关键字解析:符号引用和直接引用的区别
在编译的过程中,每个java文件都会编译成Class文件,但是编译的时候,虚拟机并不知道所引用类的地址,所以就用符号代替,而在解析这个阶段就是将符号引用转化为直接引用的过程。
例子(个人理解):医院出生的每个小孩在保育床上之后都可能被①②③进行编号(实际情况不知,毕竟女朋友都没有,哪来的孩子),只有在孩子被爸爸妈妈带回家之后,才会真正的拥有属于自己的名字,并出现在家庭的户口本上,从此孩子的名字就代表了这个孩子,而在这之前代表这个孩子的就是①②③编号。上述的①②③就代表符号引用,孩子的姓名就是直接引用。
在连接的准备阶段已经对类的静态内容附过一次初始值,而在初始化阶段则是按照程序员开发的逻辑进行初始化
【例子】
- public static int a1 = 1;
- public static int a2 = 2;
- static{
- a3 = 3;
- }
- 在连接的准备阶段:a1、a2和a3赋值0,而在初始化阶段才复制为1、2、3
使用阶段包含主动引用和被动引用
主动引用会引发类的初始化、而被动引用不会
在类使用完之后,如果满足下面的情况,类就会被卸载
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了
JVM设计者,把连接阶段的通过类的全限定名获取类的二进制流的过程,放在虚拟机之外去执行,以便程序根据需要,自己决定如何获取类,实现这个动作的模块成为类的加载器
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。
数据类型 | 关键字 | 内存占用 | 取值范围 |
字节型 | 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升的壶,肯定会报错的
int a = Integer.parseInt(str);
int a = Integer.valueOf(str).intValue();
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;
- }
由于没有break语句,程序会一直向后走,不会在判断case,也不会理会break,直接运行完整体switch。
- for(初始化表达式①; 布尔表达式②; 步进表达式④){
- 循环体③
- }
- while(布尔表达式){
- 布尔表达式为真,执行方法体
- }
- do{
- 执行方法体
- }while(布尔表达式)
while只有表达式为真的时候,才会执行方法体,do while最少执行一次方法体布尔表达式为真的时候,会执行第二次
①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};
数组的索引是从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;
- }
随机生成0-10的随机数
- Random r = new Random();
- int number = r.nextInt(10);
- 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属性
这是父类的方法
封装、继承、多态、抽象
把属性和方法结合在一起就是封装
通过extends子类可以拥有父类的非private属性和方法,并且可以对父类的方法进行重写,继承是类在功能上的扩展,继承是单继承,但是可以多重继承,但是继承使得代码的耦合度变大,使得代码的独立性变差
多态分为两种方式,分别为运行时多态和编译时多态
(1)方法重载就是编译时多态,在代码编译的时候就已经确定好,要执行哪个方法
(2)重写时运行时多态,只有当程序运行起来才能知道,到底要调用哪个子类
抽象是对一类事务的描述,并不是对具体对象的描述,例如人类,这个描述并不包含某个真实的个体,而实对全体人类的一个统称
①抽象类不能实例化
②抽象类中可以有非抽象方法,有抽象方法的类一定是抽象类
③抽象类中的方法只有方法名,没有方法体
④构造方法和静态方法不能声明为抽象方法
⑤抽象类的子类必须重写抽象类的抽象方法,给出具体的实现,除非子类也是抽象类
⑥抽象类中的内容不能用private修饰,因为抽象类中的内容就是要被子类具体实现的
接口特性
①接口可以多重继承
②接口不能实例化
③接口中没有构造方法
④接口中的方法都是抽象方法
接口的变量的特性
①接口中的变量都是static或者final修饰的,默认是public static final修饰的,不能声明为public意外的访问权限
接口方法的特性
①接口中的方法默认是public abstract修饰的
注:JDK 1.8 以后,接口里可以有静态方法和方法体了。
接口和抽象类的概念不一样。这个可以理解为接口是对动作的抽象,抽象类是对本质的抽象
- ①抽象类表示的是:这个对象是什么。
-
- ②接口表示的是,这个对象能做什么。
-
- 例子:男人,女人,这两个类,他们的抽象类是人。说明,他们都是人。人可以吃东西,动物也可以吃东西,你可以把“吃东西”定义成一个接口,因为不论是人还是动物吃东西是共性,用接口没问题
-
- 在Java语言上,一个类只能继承一个类(抽象类),类是对一类事物特征的描述,人和动物不是一类事物,但是具有相同的动作,所以描述人或者动物可以使用抽象,抽象类也是类
-
- 总结:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
静态初始化块中不能访问非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优化方式
2.导致索引失效的情况
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>
客户端与服务器端进行传输数据,简称超文本传输协议,本质是基于socket tcp协议,支持html,json,text,流。到那时最终都是基于二进制传输。
客户端,通过request请求服务器端,然后服务器端将结果相应(response)给客户端
客户端向服务器端发送请求没有事务的管理机制,就算发送请求的过程当中客户端和服务器端断开了,也不会发生任何回滚。
socket客户端向socket服务器端发送请求,服务器端需要提供IP和端口号,而且是tcp协议,需要三次握手和四次释放,本质是通过二进制流进行传出的,所以socket通讯和使用的语言(java,c)没有任何的关系
请求方式(http RESTfu风格 psot delete put)
Request Method:GET
【小知识】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中值的位置。
企业中的黑名单白名单其实就是判断请求头的来源
图中的JSESSIONID就是创建session的ID(设置的时候在response里面将值相应给浏览器)
获取的时候在请求头(Request)中获取
session的值是存放在服务器的内存当中
session原理图
创建:session的值是存放在服务器的内存当中,session创建之后会将sessionID通过请求头的方式返回给客户端保存
获取:客户端将本地的sessionID传递给服务器端,通过sessionID查找session的值
关闭浏览器SessionId会失效。但是session的值是存在服务器端的,失效的值默认是30分钟,所以关闭浏览器session并不会立即失效。
服务器停掉session值会立即失效,再次访问浏览器是获取不到的(已验证)
会话就是信息交流,会产产生信息
浏览器(客户端)和服务器之间会话的过程会产生会话数据
在客户端本地存储一些数据,当客户端访问浏览器,客户端的一些信息,例如登录信息就会保存在本地,当下次再次访问的时候可以无需再次登录
Cookie数据类型只能保存非中文字符串类型的。可以保存多个cookie,但是浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie的大小限制为4KB。
当客户端首次登录浏览器时,服务器端会创建cookie,并且将cookie的信息放在响应头中,返回给客户端;客户端获取响应头中的cookie信息,并存储在客户端的本地。当客户端再次访问网站时,客户端会将本地存放的cookie信息,通过请求头的方式,将cookie信息发送给服务器
【*】什么是幂等性
首先幂等性是数学中的概念:一个函数执行多次,返回结果相同就达到了幂等性的要求 f(f(x)) = f(x)
【*】软件开发中的幂等性
在业务上对某一事件进行多次处理,但是预期只想处理一次,例如支付过程中,购买一件商品,由于网络原因第一次没有支付成功,用户又刷新进行重复请求,此时用户的主观想法当然不是想要购买多次,此时如果发生了两次扣款,就产生了幂等性的问题。
新建一张去重表,里面可以只放一个唯一索引字段
例如订单幂等性:不能重复生成订单,订单号可以使用一些特定的规则进行生成(注意保证订生成单号的唯一性),新建一张表,只存储订单号,并将其设置为唯一索引,生成订单方法,在真正调用生成订单的业务逻辑之前,需要首先在订单去重表中,
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();
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。