java面试题

1.什么是面向对象

面向对象是一种思想,是基于面向过程而言的,就是说面向对象是将功能等通过对象来实现,将功能封装进对象之中,让对象去实现具体的细节

优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护

缺点:性能比面向过程差。

2.JDK JRE JVM

JDK: java development kit:java开发工具包

JRE:java runtime environment:java运行环境

JVM:java virtual machine:java虚拟机

1.三者联系:
JVM不能单独搞定class的执行,解释class的时候JVM需要调用解释所需要的类库lib。在JDK下面的的jre目录里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre。JVM+Lib=JRE。总体来说就是,我们利用JDK(调用JAVA API)开发了属于我们自己的JAVA程序后,通过JDK中的编译程序(javac)将我们的文本java文件编译成JAVA字节码,在JRE上运行这些JAVA字节码,JVM解析这些字节码,映射到CPU指令集或OS的系统调用。

2.三者区别:
a.JDK和JRE区别:在bin文件夹下会发现,JDK有javac.exe而JRE里面没有,javac指令是用来将java文件编译成class文件的,这是开发者需要的,而用户(只需要运行的人)是不需要的。JDK还有jar.exe, javadoc.exe等等用于开发的可执行指令文件。这也证实了一个是开发环境,一个是运行环境。
b.JRE和JVM区别:JVM并不代表就可以执行class了,JVM执行.class还需要JRE下的lib类库的支持,尤其是rt.jar。

3.==和equals比较

equals操作表示的是两个变量是否是对同一个对象的引用,即堆中的内容是否相同。==对8种基本数据类型比较的是值是否相等,对引用数据类型比较的是两个对象的地址,equals比较的是两个对象的内容。

4.hashCode与equals

equals()方法是用来判断其他的对象是否和该对象相等.

hashCode()方法给对象返回一个hash code值。这个方法被用于hash tables,例如HashMap。

它的性质是:

  • 在一个Java应用的执行期间,如果一个对象提供给equals做比较的信息没有被修改的话,该对象多次调用hashCode()方法,该方法必须始终如一返回同一个integer。
  • 如果两个对象根据equals(Object)方法是相等的,那么调用二者各自的hashCode()方法必须产生同一个integer结果。
  • 并不要求根据equals(java.lang.Object)方法不相等的两个对象,调用二者各自的hashCode()方法必须产生不同的integer结果。然而,程序员应该意识到对于不同的对象产生不同的integer结果,有可能会提高hash table的性能。

"童话""重工",这两个词hash值相等。

5.final

在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。

当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。

修饰变量是final用得最多的地方,对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的。

很多时候会容易把static和final关键字混淆,static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变。

6.String、StringBuffer、StringBuilder

  1. String类是一个不可变类,即创建String对象后,该对象中的字符串是不可改变的,直到这个对象被销毁。StringBuffer与StringBuilder都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,是可变类。当一个字符串经常被修改时,最好使用StringBuffer实现。如果用String保存一个经常被修改的字符串,该字符串每次修改时都会创建新的无用的对象,这些无用的对象会被垃圾回收器回收,会影响程序的性能,不建议这么做。

  2. 当创建String对象时,可以利用构造方法String str = new String("Java")的方式来对其进行初始化,也可以直接用赋值的方式String s = "Java"来初始化。而StringBuffer只能使用构造方法StringBuffer sb = new StringBuffer("hello")的方式初始化。

  3. String字符串修改方法是首先创建一个StringBuffer,其次调用StringBuffer的append方法,最后调用StringBuffer的toString()方法把结果返回,示例代码如下:

    String str = "hello";
    str += "java";
    

    以上代码等价于下面的代码:

    StringBuffer sb = new StringBuffer(str);
    sb.append("java");
    str = sb.toString();
    

    上述String字符串的修改过程要比StringBuffer多一些额外操作,会增加一些临时的对象,从而导致程序的执行效率降低。StringBuffer和StringBuilder在修改字符串方面比String的性能要高。

  4. String实现了equals()方法和hashCode()方法,new String("java").equals(new String("java"))的结果为true;

    而StringBuffer没有实现equals()方法和hashCode()方法,因此,new StringBuffer("java").equals(new StringBuffer("java"))的结果为false,将StringBuffer对象存储进Java集合类中会出现问题。

  5. StringBuffer与StringBuilder都提供了一系列插入、追加、改变字符串里的字符序列的方法,它们的用法基本相同,只是StringBuilder是线程不安全的,StringBuffer是线程安全的,。如果只是在单线程中使用字符串缓冲区,则StringBuilder的效率会高些,但是当多线程访问时,最好使用StringBuffer。

综上,在执行效率方面,StringBuilder最高,StringBuffer次之,String最低,对于这种情况,一般而言,如果要操作的数量比较小,应优先使用String类;如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先使用StringBuilder类。

7.重载和重写的区别

  1. Override 特点

  1、覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;

  2、覆盖的方法的返回值必须和被覆盖的方法的返回一致;

  3、覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类,不能比父类抛出更多的异常;

  4、被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。

2.Overload 特点

  1、在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是fun(int, float), 但是不能为fun(int, int));

  2、不能通过访问权限、返回类型、抛出的异常进行重载;

  3、方法的异常类型和数目不会对重载造成影响;

  4、对于继承来说,如果某一方法在父类中是访问权限是priavte,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果。

8.接口和抽象类的区别

抽象类:在Java中被abstract关键字修饰的类称为抽象类,被abstract关键字修饰的方法称为抽象方法,抽象方法只有方法的声明,没有方法体。抽象类的特点:

a、抽象类不能被实例化只能被继承;

b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;

c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;

d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;

e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。

接口:Java中接口使用interface关键字修饰,特点为:

a、接口可以包含变量、方法;变量被隐式指定为public static final,方法被隐式指定为public abstract(JDK1.8之前);

b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;

c、一个类可以实现多个接口;

d、JDK1.8中对接口增加了新的特性:(1)、默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;(2)、静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。

相同点

(1)都不能被实例化 (2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。

不同点

(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。

(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。

(3)接口强调特定功能的实现,而抽象类强调所属关系。

(4)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。

9.List和Set的区别

1、List,Set都是继承自Collection接口
2、List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
3.Set和List对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

10.ArrayList和LinkedList区别

ArrayList基于动态数组实现的非线程安全的集合;LinkedList基于链表实现的非线程安全的集合。
对于随机index访问的get和set方法,一般ArrayList的速度要优于LinkedList。因为ArrayList直接通过数组下标直接找到元素;LinkedList要移动指针遍历每个元素直到找到为止。
新增和删除元素,一般LinkedList的速度要优于ArrayList。因为ArrayList在新增和删除元素时,可能扩容和复制数组;LinkedList实例化对象需要时间外,只需要修改指针即可。
LinkedList集合不支持高效的随机访问(RandomAccess)
ArrayList的空间浪费主要体现在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

11.HashMap和HashTable有什么区别?其底层实现是什么?

区别:

1、继承的父类不同
HashTable继承Dictionary类,而hashMap继承了AbstractMap类,但是二者都实现了map接口。

2、线程安全性不同
Hashtable 线程安全,因为它每个方法中都加入了Synchronize。HashMap是线程不安全的。
HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。在hashmap做put操作的时候可能会造成数据丢失。现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失

3、是否提供contains方法
HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。

Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。

4、key和value是否允许null值
Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。

HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

5、两个遍历方式的内部实现上不同
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

6、hash值不同
哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。

hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。

Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。

7、内部实现使用的数组初始化和扩容方式不同
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。

Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。

HashTable中hash数组默认大小是11,增加的方式是 old*2+1。

HashMap中hash数组默认大小是16,增加的方式是 old*2。

HashMap的底层原理:

1、为什么用HashMap?
HashMap是一个散列桶(数组和链表),它存储的内容是键值对(key-value)映射
HashMap采用了数组和链表的数据结构,能在查询和修改方便继承了数组的线性查找和链表的寻址修改
HashMap是非synchronized,所以HashMap很快
HashMap可以接受null键和值,而Hashtable则不能
2、HashMap的工作原理是什么?
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的bucket位置来储存Node 对象。这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Node 。

![img]( https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zczAuYmFpZHUuY29tLzZPTldzamlwMFFJWjh0eWhucS9pdC91PTU1NzkwNjM1MiwxNDk0NjAzMzI0JmZtPTE3MyZhcHA9NDkmZj1KUEVH?x-oss-process=image/format ,png)

以下是HashMap初始化 ,简单模拟数据结构

Node[] table=new Node[16] 散列桶初始化,tableclass Node { hash;//hash值 key;//键 value;//值 node next;//用于指向链表的下一层(产生冲突,用拉链法)}

以下是具体的put过程(JDK1.8版)

1、对Key求Hash值,然后再计算下标

2、如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)

3、如果碰撞了,以链表的方式链接到后面

4、如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表

5、如果节点已经存在就替换旧值

6、如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)

以下是具体get过程(考虑特殊情况如果两个键的hashcode相同,你如何获取值对象?)当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

![img]( https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zczIuYmFpZHUuY29tLzZPTllzamlwMFFJWjh0eWhucS9pdC91PTQwODMwNTQwMDgsNDE3OTM1MDc1JmZtPTE3MyZhcHA9NDkmZj1KUEVH?x-oss-process=image/format ,png)

3、有什么方法可以减少碰撞?
扰动函数可以减少碰撞,原理是如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这就意味着存链表结构减小,这样取值的话就不会频繁调用equal方法,这样就能提高HashMap的性能。(扰动即Hash方法内部的算法实现,目的是让不同对象返回不同hashcode。)使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。为什么String, Interger这样的wrapper类适合作为键?因为String是final的,而且已经重写了equals()和hashCode()方法了。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。

4、HashMap中hash函数怎么是是实现的?
我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式,我们来看看JDK1.8的源码是怎么做的

static final int hash(Object key) {
    if (key == null){ return 0; } 
    int h; 
    h=key.hashCode();//返回散列值也就是hashcode 
    // ^ :按位异或 
    // >>>:无符号右移,忽略符号位,空位都以0补齐 
    //其中n是数组的长度,即Map的数组部分初始化长度 
    return (n-1)&(h ^ (h >>> 16));}

![img]( https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zczIuYmFpZHUuY29tLzZPTllzamlwMFFJWjh0eWhucS9pdC91PTEyMzA5Nzk5NDAsMzgwNDgxOTczNyZmbT0xNzMmYXBwPTQ5JmY9SlBFRw?x-oss-process=image/format ,png)

简单来说就是

1、高16bt不变,低16bit和高16bit做了一个异或(得到的HASHCODE转化为32位的二进制,前16位和后16位低16bit和高16bit做了一个异或)

2、(n-1)&hash=->得到下标

5、拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

6、说说你对红黑树的见解?

![img]( https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zczIuYmFpZHUuY29tLzZPTllzamlwMFFJWjh0eWhucS9pdC91PTE3NDkyODQ1NDAsNDA1NzI2NjU2NCZmbT0xNzMmYXBwPTQ5JmY9SlBFRw?x-oss-process=image/format ,png)

  1. 每个节点非红即黑

  2. 根节点总是黑色的

  3. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

  4. 每个叶子节点都是黑色的空节点(NIL节点)

  5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

7、解决hash 碰撞还有那些办法?
开放定址法。

当冲突发生时,使用某种探查技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的地址。

按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。

下面给一个线性探查法的例子

问题:已知一组关键字为(26,36,41,38,44,15,68,12,06,51),用除余法构造散列函数,用线性探查法解决冲突构造这组关键字的散列表。

解答:为了减少冲突,通常令装填因子α由除余法因子是13的散列函数计算出的上述关键字序列的散列地址为(0,10,2,12,5,2,3,12,6,12)。

前5个关键字插入时,其相应的地址均为开放地址,故将它们直接插入T[0],T[10),T[2],T[12]和T[5]中。

当插入第6个关键字15时,其散列地址2(即h(15)=15%13=2)已被关键字41(15和41互为同义词)占用。故探查h1=(2+1)%13=3,此地址开放,所以将15放入T[3]中。

当插入第7个关键字68时,其散列地址3已被非同义词15先占用,故将其插入到T[4]中。

当插入第8个关键字12时,散列地址12已被同义词38占用,故探查hl=(12+1)%13=0,而T[0]亦被26占用,再探查h2=(12+2)%13=1,此地址开放,可将12插入其中。

类似地,第9个关键字06直接插入T[6]中;而最后一个关键字51插人时,因探查的地址12,0,1,…,6均非空,故51插入T[7]中。

8、如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为<原下标+原容量>的位置

9、重新调整HashMap大小存在什么问题吗?
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。(多线程的环境下不使用HashMap)

10、为什么多线程会导致死循环,它是怎么发生的?
HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。1.扩容:创建一个新的Entry空数组,长度是原数组的2倍。2.ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

12.ConcurrentHashMap原理,jdk7和jdk8版本的区别?

  • jdk7:

    数据结构:ReentrantLock+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。

    元素查询:二次Hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。

    锁:Segment分段锁。Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为Segment个数,可以通过构造函数指定,数组扩容不会影响其他的Segment。

    get方法无需加锁,volatile保证

  • jdk8:

    数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性。

    查找,替换,赋值操作都使用CAS

    锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容。

    读操作无锁:

    Node的val和next使用volatile修饰,读写线程对该变量互相可见

    数组用volatile修饰,保证扩容时被读线程感知

13.什么是字节码?采用字节码的好处是什么?

字节码:

Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。

采用字节码的好处:

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

14.Java中的异常体系

1 异常的继承体系结构

img

  • Throwable 类是 Java 语言中所有错误或异常的超类。
  • 只有当对象是此类(或其子类之一)的实例时,才能通过 Java 虚拟机或者 Java throw 语句抛出。类似地,只有此类或其子类之一才可以是 catch 子句中的参数类型。
  • Throwable 包含了其线程创建时线程执行堆栈的快照。它还包含了给出有关错误更多信息的消息字符串。
  • 最后,它还可以包含 cause(原因):另一个导致此 throwable 抛出的 throwable。此 cause 设施在 1.4 版本中首次出现。它也称为异常链 设施,因为 cause 自身也会有 cause,依此类推,就形成了异常链,每个异常都是由另一个异常引起的。

1.1 Error

  • Error 是 Throwable 的子类,用于指示合理的应用程序不应该试图捕获的严重问题。
  • 大多数这样的错误都是异常条件。虽然 ThreadDeath 错误是一个“正规”的条件,但它也是 Error 的子类,因为大多数应用程序都不应该试图捕获它。
  • 在执行该方法期间,无需在其 throws 子句中声明可能抛出但是未能捕获的 Error 的任何子类,因为这些错误可能是再也不会发生的异常条件。
  • Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。

1.2 Exception

  • Exception 异常主要分为两类
    • 一类是 IOException(I/O 输入输出异常),其中 IOException 及其子类异常又被称作「受查异常」
    • 另一类是 RuntimeException(运行时异常),RuntimeException 被称作「非受查异常」。
  • 受查异常就是指,编译器在编译期间要求必须得到处理的那些异常,你必须在编译期处理了。

2 自定义异常类型

Java 的异常机制中所定义的所有异常不可能预见所有可能出现的错误,某些特定的情境下,则需要我们自定义异常类型来向上报告某些错误信息。

  • 在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。
    • 所有异常都必须是 Throwable 的子类。
    • 如果希望写一个检查性异常类,则需要继承 Exception 类。
    • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。

3 异常的处理方式

3.1 try...catch关键字

  • 使用 try 和 catch 关键字可以捕获异常。
  • try/catch 代码块放在异常可能发生的地方。

try/catch代码块中的代码称为保护代码,使用 try/catch 的语法如下:

try {
   // 程序代码
} catch(ExceptionName e1) {
   //Catch 块
}
  • Catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样。
  • 一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。
  • 多重捕获块的语法如下所示:
try{
   // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}

3.2 throws/throw 关键字

  • 如果一个方法没有捕获一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。也可以使用 throw 关键字抛出一个异常,无论它是新实例化的还是刚捕获到的。
  • 下面方法的声明抛出一个 RemoteException 异常:
public class className {
  public void deposit(double amount) throws RemoteException {
    // Method implementation
    throw new RemoteException();
  }
  //Remainder of class definition
}

一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。

3.3 finally关键字

  • finally 关键字用来创建在 try 代码块后面执行的代码块。
  • 无论是否发生异常,finally 代码块中的代码总会被执行。在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。
  • finally 代码块出现在 catch 代码块最后,语法如下:
try{
  // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}finally{
  // 程序代码
}

15.Java类加载器

一、类的加载过程

首先看一张类加载以及前后过程的流程图

![类加载过程]( https://img-blog.csdnimg.cn/92dd0b7afc964c28b487c0199d811cf3.png?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01TU1BMQU5FVA==,size_16,color_FFFFFF,t_70)

流程图中可以看到一个类的生命周期分为:加载→验证→准备→解析→初始化→使用→卸载七个阶段,其中标绿的五个阶段为一个类的完整加载过程,这五个步骤统称为类的加载,以下着重对这五个步骤进行简单介绍

加载:通过类的全名在硬盘上搜索并获取其二进制字节流,并将其读入JVM方法区,同时在堆内存中创建该类的Class对象。
验证:通过文件格式验证、元数据验证、字节码验证和符号引用验证四个验证来确保被加载类的正确性,保证.class文件中的二进制字节流符合JVM要求且不会危害JVM。
准备:仅为类中的静态变量(被static修饰的变量)分配内存并初始化为默认值,不会为普通成员变量分配内存,其中用被final static修饰的变量在编译阶段就已经完成分配。
解析:将常量池中的符号引用转换为直接引用。
初始化:若有该类有父类未被初始化,则先初始化其父类,随后为类的静态变量赋初始设定值,执行静态代码块。

二、类加载器

类加载器负责将.class文件加载到JVM内存中,并生成与之相对应的Class对象。
类加载器一共分为三种:启动类加载器、扩展类加载器、应用类加载器。

启动类加载器(BootstrapClassLoader):也叫引导类加载器,本加载器由C++实现,负责加载jre/lib目录下的核心类库,比如rt.jar、charsets.jar等。
注:JAVA的沙箱安全机制严格限制代码对本地资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱安全机制体现在启动类加载器上就是该启动类只会加载指定包(java、sun等包)下的类,且会优先加载JDK自带的类,同包同名外部类也不会被加载,保护了JAVA核心源代码。
扩展类加载器(ExtClassLoader):负责加载jre/lib/ext目录中的JAR类包(通常为非原生被引用的jar包)。
应用类加载器(AppClassLoader):也叫系统类加载器,负责加载用户自定义路径下的class字节码文件,通常该加载器为程序默认加载器。
除了以上三种类加载器,我们还可以自定义类加载器,根据需求扩展类的加载方式,后文中会进行演示。

三、双亲委派机制

双亲委派机制如下图所示、

![在这里插入图片描述]( https://img-blog.csdnimg.cn/65810c93237f4bd7805a15896106bb2e.png?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L01TU1BMQU5FVA==,size_16,color_FFFFFF,t_70)

可以简单吧双亲委托机制看成两个部分:委托和加载(非官方,作者个人理解)

  • 委托:当收到类加载请求时,最先调用应用类加载器(前文中有提及,通常应用类加载器为程序中的默认加载器),判断此前应用类加载器是否加载过该类,若没有加载过,则向上委托给其父类扩展类加载器,扩展类加载器执行同样的操作,若扩展类加载器也没有加载过,则向上委托给其父类启动类加载器,同样先执行判断操作。在以上三次判断操作中,若发现曾加载过此类,则直接返回,不再向上进行委托;若直到启动类加载器也没有加载过该类,则进行加载操作。

  • 加载:加载操作由启动类加载器先开始,启动类先搜索jre/lib目录下的核心类库中是否有该类,若有,加载后返回,若没有,则由其子类继续执行加载操作,扩展类加载器和应用类加载器同理。

注:启动类加载器、扩展类加载器以及应用类加载器之间并无继承关系,提及父类子类是因为在ClassLoader类中存在一个parent变量,后文中会提及。

使用双亲委派机制的优点:

先判断再加载的机制避免了类的重复加载问题
加载时,加载顺序为启动类加载器→扩展类加载器→应用类加载器,实质上是从核心类库到自定义类的加载顺序,这样的顺序解决了一些安全问题,比如用户自定义了一个String类,这与java.lang包下的String类同名,使用双亲委派机制就会优先加载JDK自带的String类而忽视用户自定义的String类,防止JAVA核心代码被篡改

16.GC如何判断对象可以被回收

GC Roots可达性分析法:从GC Roots向下搜索、追溯,当一个对象不再和任何的一个GC Root产生关系,就会被判定为垃圾。

![img]( https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zMC5sZ3N0YXRpYy5jb20vaS9pbWFnZTMvTTAxLzYyLzEyL0NncE9JRjRoZVZ1QVByV1ZBQUNLM3FyQTktMDAxMS5wbmc?x-oss-process=image/format ,png)

如图中的 Obj5、6、7,被判定为垃圾。

posted @ 2021-08-19 10:00  shanaprprpr  阅读(59)  评论(0)    收藏  举报