java面试题集锦(一)

来源:互联网 发布:网络剪刀手教程 编辑:程序博客网 时间:2024/05/21 06:59

2016.7.19开更...........................................................................

(1):equals和==的区别

        equals是用于比较两个实例所指向的内存空间里面的值是否相等;

        ==用于两个实例是否指向同一内存空间;

(2):String与StringBuffer、StringBuffer和StringBuilder的区别

         String与StringBuffer的区别通俗的讲就是常量和变量的区别,String的内容一旦赋值之后是不能修改的,可能你觉得使用“+“运算符是在修改String的值,其实不是这样的“+“其实是要重新在内存里面开辟一块空间来存放最新的String内容的;但是StringBuffer却不是这样的,因为他的底层实现是char数组,所以对于追加、删除之类的操作来说他只是在原有的内存空间上面进行的,因此比较来看,如果你是频繁的进行字符串的拼接或者删除的话,建议还是使用StringBuffer效率更高点,当然StringBuilder也是可以的;

        再来说说StringBuffer和StringBuilder的区别,两者最大的区别在于StringBuffer是线程安全的,StringBuilder是非线程安全的,两者的底层实现都是char数组,所以在进行追加、删除操作的时候同样都不会开辟新的内存空间的;

(3):overload和override的区别

        overload指的是重载,具体存在于方法名字相同,但是方法参数类型、参数个数、参数顺序至少有一个不同的方法之间,但是除方法返回值之外全部其他都相同的两个方法不能称之为重载,重载可以存在于同类里面,也可以存在于父类和子类之间;

        override指的是重写,两个方法称之为重写的条件的:两个方法的名称、参数个数、参数类型、参数顺序都是一样的,并且两个方法分别位于父类和子类,子类中的方法不能缩小父类方法定义的访问权限,也不能抛出比父类方法更多的异常,如果父类的方法前有final关键字修饰的话,那么子类就不能重写该方法;

        其中overload重载称为编译时多态,override重写称为运行时多态,他们是多态的两种形式;

(4):抽象类和接口的区别

        抽象类属于类,那么很明显继承了他的类就不能再继承别的类了,不然就违反了单继承要求,而接口却可以多继承、多实现;

        抽象类里面可以有抽象方法和实现方法,但是接口里面的方法必须是抽象的;

        抽象类里面可以有构造函数,但是接口里面不可以;

        抽象类里面的抽象方法可以用public、protected修饰,但是接口里面的方法都是public类型的;

        抽象类中可以定义public、private、protected类型的变量,但是接口里面的变量只要你定义了默认都是public static final类型的;

(5):hashCode与equals的区别

        见博客:hashMap和equals的区别

(6):HashMap实现原理

        HashMap是数组+链表实现的,既然用到hash散列,那么肯定不可避免的会出现冲突问题,HashMap解决冲突的方法是拉链法,因为这里有用到数组,那么当容量不足的时候就需要进行扩容操作了,在HashMap中有个术语叫冲突,当冲突几率越来越高的时候就需要进行扩容操作了,那什么情况就叫冲突几率高呢?就是当我们的数组元素个数超过了数组原先大小*装填因子,默认情况下的装填因子是0.75,扩容有个坏处就是每次扩容之后都必须重新计算原先数组中的元素在新数组中的存储位置,这点比较消耗性能,所以一般情况下如果你已经能够确定最大需要多大散列范围的数组的话,建议还是能够指定大小;

        接下来就是HashMap的put和set原理了:

        put操作和set操作进行操作的对象是主要是key,如果你查看源码的话会发现value只是跟着key的步伐在走而已,并没有实质性的进行操作,对于put操作,首先会计算出当前key对应的hash值,接着找到计算出来的hash值在数组中的下标位置,查看该下标位置处对应的链表是否为null,为空的话直接将当前键值对插入到该链表首位,不会执行当前key对象的equals方法;如果下标位置处对应的链表不为null的话,会通过for循环来通过key的equals方法来查看这个链表中有没有与当前键值相等的键值对Entry存在,有的话,会用当前值替换原先这个键值对的value值,遍历结束如果不存在的话,会将当前键值对插入到链表的头部,这个就是put过程了;

        get操作过程思想可以借助于put过程,首先会计算出当前key值的hash值,接着找到此hash值在数组中的位置,找到这个位置对应的链表,接着通过for循环遍历这个链表,遍历过程中调用equals方法查看有没有等于当前key的键值对存在,有的话直接返回这个键值对对应的value值即可;

        HashMap数据结构原理图:

                       

        HashMap注意点:

        HashMap是非线程安全的,也就是说你在使用迭代器的过程中有其他线程修改了map的话,你的程序可能会抛出ConcurrentModificationException异常,这就是我们常见的fail-fast机制了,原因在于我们在调用HashMap的迭代器里面的每个方法的时候,都会通过判断原先map被修改次数和当前被修改次数是否相等,不等的话直接就抛出了ConcurrentModificationException异常了,这点在ArrayList里面使用迭代器也会出现,具体解决方法就是使用ConcurrentHashMap代替HashMap了;

        HashMap是允许你的键或者值为null的;

        HashMap是不能保证随着时间的推移,你里面元素之间的顺序不变,原因就在于map中存放hash值的数组在扩容的时候会重新计算原先元素在新数组中位置的;

(7):HashTable和HashMap的区别

        下面分别从线程安全性、键值是否允许为空、效率几个角度进行区分:

        HashTable是线程安全的具体线程安全这点是怎么实现的呢?通过synchronized关键字,这也就是后来他被ConcurrentHashMap替代的原因了,效率不是很高呗,每次想要对HashTable进行更新操作都需要获得同步锁这点很烦人的,而且锁住的是整个hash表,但是ConcurrentHashMap的实现就比较灵活了,他每次锁住的是你将要操作的那个hashtable表,具体原理分析在问题(8)给出;而HashMap是非线程安全的;

        HashTable不允许key或者value值为null,但是HashMap是允许的;

        因为HashTable在使用的时候涉及到了加锁操作,势必会带来性能上的损失,效率是不如HashMap的,如果你已经明确知道是在单线程环境下使用map的话,建议直接使用HashMap;

        HashTable使用的是Enumerator迭代器,不是fail-fast机制的,而HashMap使用的是Iterator迭代器,会出现fail-fast现象;

(8):ConcurrentHashMap实现原理

        从JDK1.5开始就出现了ConcurrentHashMap类用来解决HashMap非线程安全这个问题,虽然之前已经有HashTable的存在,但是因为他是使用synchronized锁住整个Hash表的,这样会带来很多性能上的不足,而ConcurrentHashMap将锁粒度细化,因此效率上相对来说更优,他的实现原理是这样的:一个ConcurrentHashMap由多个Segment组成,每一个Segment是包含了一个HashEntry数组的小HashTable,每一个Segment包含了对自己Hashtable的一组操作,比如put,get之类的,他的数据结构原理图是这样子的:

                       

        你会发现ConcurrentHashMap其实是通过Segment将大的HashTable分割成一个个的小HashTable了,这样做的目的是为了将加锁操作作用在小的HashTable上面,不至于把整个HashTable全都锁起来;

        我们来看看他的put方法是怎么执行的,首先计算出当前key值对应的hash值,找到该hash值对应的Segment位置,接下来调用的就是该位置Segment的put方法了,进入这个方法就会进行加锁操作,也就是从上面的绿色部分开始进行加锁操作,在Segment的put方法里面会传入刚刚key值的hash值,通过(tab.length - 1) & hash获得当前hash值在当前Segment对应的HashEntry数组中的位置,找到HashEntry数组的位置之后获得该位置处对应链表的第一个HashEntry值,接着就开始通过equals方法来看看该链表中是否有与当前key值equals的键值对存在了,有的话直接替换值,没有的话插入到当前链表的第一个位置,这点和HashMap是一致的,最后在put方法结束的finally中释放掉锁就可以了,这个是put过程,至于get过程和put过程原理类似,不再阐述;

(9):HashSet实现原理

        在HashSet中你会发现存在一个属性类型为HashMap的变量,而且他的所有方法的真正操作都是这个map来执行的,也就是说他底层实现其实就是HashMap了,那么既然作为Set,它里面的元素是肯定不能相等的,这里的相等指的是两个对象的执行hashCode和equals方法之后的返回值都一样,那么HashSet是怎么保证这一点的呢?通过把要加入HashSet的值作为HashMap的key值来保证,我们来看看HashSet中的add源码:

<span style="font-family:Comic Sans MS;">public boolean add(E e) {        return map.put(e, PRESENT)==null;    }</span>
        很简单,你平常所add进来的值其实是以key的形式存在在HashMap中的,而这个key对应的value是PRESENT,这个值的定义是:
<span style="font-family:Comic Sans MS;"> // Dummy value to associate with an Object in the backing Map    private static final Object PRESENT = new Object();</span>
        从上面的注释就可以看出来PRESENT只是一个虚设的,只是为了保证key值对应的value值不为null而已,除此之外没什么作用;HashSet充分利用了HashMap的key值唯一这个特点进行重复元素的筛选,还有一点需要注意add方法的返回值表示的是HashMap中是否存在对应元素的key,不存在返回true,存在返回false;

(10):ArrayList和LinkedList对比

         ArrayList的底层实现是动态数组,因而他的空间开销主要体现在需要为数组的扩充而预留的空间上;LinkedList的底层实现是双向链表,因而他的主要开销体现在需要存储指向前后元素的指针上面;

        ArrayList的底层实现是数组,也就造成了他适合随机访问的有点和不适合插入删除的缺点;LinkedList的底层实现是链表,也就造成了他适合插入删除的有点和不适合随机访问的缺点了;

        LinkedList实现了Deque接口,那么他也就可以用来作为队列以及双端队列使用了;

        ArrayList和LinkedList都是非线程安全的,那么相应的在多线程环境下使用的过程中他们都可能会出现fail-fast现象抛出ConcurrentModificationException异常,如果在多线程环境下使用他们的话,可以用CopyOnWriteArrayList代替ArrayList,用ConcurrentLinkedQueue代替LinkedList;

        因为ArrayList的底层是采用动态数组实现的,所以不可避免的在使用的过程中当数组容量不足的话就要进行扩容,而扩容的过程中将伴随着数组元素的拷贝,这点相对来说是是有损性能的,所以如果你已经明确知道最大要多大的容量最好是在初始化ArrayList的时候直接指定大小;而LinkedList是不存在这个问题的,毕竟是链表嘛,你内存有多大我就可以死劲添加元素,并不用关心我自己到底要放多少元素啦;

        具体更详细的区别可以看我的这篇博客;

(11):java内存模型

        在计算机中鉴于处理器的处理速度要远远高于主存也就是我们常说的内存,因此引入了高速缓存的概念,他有效的缓和了处理速度与存储空间大小之间的矛盾,这点在单处理器环境下是没什么问题的,但是换做多处理器的场景也就是我们现在所说的多核情况下,问题就出现了,大家用各自的处理器高速缓存进行计算但是内存空间都是共享的,所以经常会出现数据不一致问题,因此计算机领域存在着缓存一致性的协议,在我们的java内存模型中,同样也能见到类似的身影,java内存模型中规定变量要存储在主内存中,但是不同的线程内部都有各自的工作内存,这点就类似于前面说到的处理器内部的高速缓存了,在本地内存中存储的是该线程自己用到的主内存变量的副本,线程操作的变量都是自己本地内存的,不能直接去操作主内存的变量,也不能访问另一个线程内部的本地内存里面存储的变量,两个线程之间变量值的传递是需要主内存做中介的,这有点类似于在河两边的人想要达到各自对岸的话都必须通过桥才能实现,这里的桥就是主内存了;也正是因为这个原因,所以会导致多线程之间如果操作同一个变量的话会导致变量的值不确定;


      

5 1
原创粉丝点击