Java名企高频率面试题及答案 精心整理(二)

11.HashMap和ConcurrentHashMap的区别,HashMap的底层源码

  • Hashmap本质是数组加链表。根据key取得hash值,然后计算出数组下标,如果多个key对应到同一个下标,就用链表串起来,新插入的在前面。

    ConcurrentHashMap:在hashMap的基础上,ConcurrentHashMap将数据分为多个segment(类似hashtable),默认16个(concurrency level),然后在每一个分段上都用锁进行保护,从而让锁的粒度更精细一些,并发性能更好。

  • 一、HashMap概述

     HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

      值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。

    Map map = Collections.synchronizedMap(new HashMap());

    二、HashMap的数据结构

     HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置,能够很快的计算出对象所存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。学过数据结构的同学都知道,解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。

    技术分享

    从上图中可以看出,HashMap底层就是一个数组结构,数组中存放的是一个Entry对象,如果产生的hash冲突,也就是说要存储的那个位置上面已经存储了对象了,这时候该位置存储的就是一个链表了。我们看看HashMap中Entry类的代码:

    <K key; V value; Entry<K,V> next; final int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; //hash值冲突后存放在链表的下一个 key = k; hash = h; } ......... }

    HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表。

    三、HashMap源码分析

      先看看HashMap类中的一些关键属性:

    size;loadFactor; modCount;//被修改的次数

    其中加载因子是表示Hash表中元素的填满的程度.若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.反之,加载因子越小,填满的元素越少,
    好处是:冲突的机会减小了,但:空间浪费多了.冲突的机会越大,则查找的成本越高.反之,查找的成本越小.因而,查找时间就越小.因此,必须在 “冲突的机会”与”空间利用率”之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的”时-空”矛盾的平衡与折衷.

      如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。

      下面看看HashMap的几个构造方法:

    public HashMap(int initialCapacity, float loadFactor) { //确保数字合法 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); (capacity < initialCapacity) //确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂 capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); }

    我们可以看到在构造HashMap的时候如果我们指定了加载因子和初始容量的话就调用第一个构造方法,否则的话就是用默认的。默认初始容量为16,默认加载因子为0.75。我们可以看到上面代码中13-15行,这段代码的作用是确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂,至于为什么要把容量设置为2的n次幂,我们等下再看。

      下面看看HashMap存储数据的过程是怎样的,首先看看HashMap的put方法:

    public V put(K key, V value) { if (key == null) //如果键为null的话,调用putForNullKey(value) return putForNullKey(value); int hash = hash(key.hashCode());//根据键的hashCode计算hash码 int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { //处理冲突的,如果hash值相同,则在该位置用链表存储 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果key相同则覆盖并返回旧值 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }

    当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

    具体的实现是:

    当你的key为null时,会调用putForNullKey,HashMap允许key为null,这样的对像是放在table[0]中。

    如果不为空,则调用int hash = hash(key.hashCode());这是hashmap的一个自定义的hash,在key.hashCode()基础上进行二次hash

    static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }

    得到hash码之后就会通过hash码去计算出应该存储在数组中的索引,计算索引的函数如下:

    static int indexFor(int h, int length) { return h & (length-1); }

     这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的n 次方,这是HashMap在速度上的优化。当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

    下面我们继续回到put方法里面,前面已经计算出索引的值了,看到第6到14行,如果数组中该索引的位置的链表已经存在key相同的对象,则将其覆盖掉并返回原先的值。如果没有与key相同的键,则调用addEntry方法创建一个Entry对象,addEntry方法如下:

    void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,将该位置原先的值设置为新entry的next,也就是新entry链表的下一个节点 table[bucketIndex] = new Entry<>(hash, key, value, e); if (size++ >= threshold) //如果大于临界值就扩容 resize(2 * table.length); //以2的倍数扩容 }

    参数bucketIndex就是indexFor函数计算出来的索引值,第2行代码是取得数组中索引为bucketIndex的Entry对象,第3行就是用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象的next构成链表。

      第4行和第5行就是判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容,HashMap扩容是扩为原来的两倍。resize()方法如下:

    void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable);//用来将原先table的元素全部移到newTable里面 table = newTable; //再将newTable赋值给table threshold = (int)(newCapacity * loadFactor);//重新计算临界值 }

    扩容是需要进行数组复制的,上面代码中第10行为复制数组,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

    12.TreeMap、HashMap、LindedHashMap的区别

    Map家族的继承关系

    技术分享

    1 . TreeMap

    TreeMap实现SortMap接口,能够把它保存的记录根据键排序, 默认是按键值的升序排序(自然顺序),也可以指定排序的比较器( Comparator ),当用Iterator 遍历TreeMap时,得到的记录是排过序的。

    注意,此实现不是同步的。如果多个线程同时访问一个映射,则其必须 外部同步。这一般是通过对自然封装该映射的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSortedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的不同步访问,如下所示:

    SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

    2 .HashMap

    键和值都可以是空对象
    不保证映射的顺序,多次访问,映射元素的顺序可能不同
    非线程安全

    3 .LinkedHashMap

    内部维持了一个双向链表,可以保持顺序

    此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射的意外的非同步访问:

    Map m = Collections.synchronizedMap(new LinkedHashMap(...));

    LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

    4 .使用场景

    一般情况下,我们用的最多的是HashMap
    HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。

    在Map 中插入、删除和定位元素,HashMap 是最好的选择。

    TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。

    LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。

    13.Collection包结构,与Collections的区别

    Collection家族

    技术分享

    Collection是集合继承结构中的顶层接口

    Collections 是提供了对集合进行操作的强大方法的工具类 ,它包含有各种有关集合操作的静态多态方法。此类不能实例化

    14.try catch finally,try里有return,finally还执行么?

    Condition 1: 如果try中没有异常且try中有return (执行顺序)

    try ---- finally --- return

    Condition 2: 如果try中有异常并且try中有return

    try----catch---finally--- return

    总之 finally 永远执行!

    Condition 3: try中有异常,try-catch-finally里都没有return ,finally 之后有个return

    try----catch---finally

    try中有异常以后,根据java的异常机制先执行catch后执行finally,此时错误异常已经抛出,程序因异常而终止,所以你的return是不会执行的

    Condition 4: 当 try和finally中都有return时,finally中的return会覆盖掉其它位置的return(多个return会报unreachable code,编译不会通过)。

    Condition 5: 当finally中不存在return,而catch中存在return,但finally中要修改catch中return 的变量值时

    int ret = 0; try{ (); } catch(Exception e) { ret = 1; return ret; } finally{ ret = 2; }

    最后返回值是1,因为return的值在执行finally之前已经确定下来了

    15.Excption与Error包结构。OOM你遇到过哪些情况,SOF你遇到过哪些情况

    Java 异常类继承关系图

    技术分享

    (一)Throwable

      Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类或其子类之一的实例时,才能通过 Java 虚拟机或者 Java throw 语句抛出,才可以是 catch 子句中的参数类型。
      Throwable 类及其子类有两个构造方法,一个不带参数,另一个带有 String 参数,此参数可用于生成详细消息。
      Throwable 包含了其线程创建时线程执行堆栈的快照。它还包含了给出有关错误更多信息的消息字符串。
      
    Java将可抛出(Throwable)的结构分为三种类型:
      错误(Error)
      运行时异常(RuntimeException)
      被检查的异常(Checked Exception)

     1.Error
      Error 是 Throwable 的子类,用于指示合理的应用程序不应该试图捕获的严重问题。大多数这样的错误都是异常条件。
      和RuntimeException一样, 编译器也不会检查Error。
      当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误,程序本身无法修复这些错误的。
      
     2.Exception
      Exception 类及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。
       对于可以恢复的条件使用被检查异常(Exception的子类中除了RuntimeException之外的其它子类),对于程序错误使用运行时异常。 
      

    ① ClassNotFoundException
      
    当应用程序试图使用以下方法通过字符串名加载类时:
    Class 类中的 forName 方法。
    ClassLoader 类中的 findSystemClass 方法。
    ClassLoader 类中的 loadClass 方法。
    但是没有找到具有指定名称的类的定义,抛出该异常。

    ② CloneNotSupportedException 当调用 Object 类中的 clone 方法复制对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。重写 clone 方法的应用程序也可能抛出此异常,指示不能或不应复制一个对象。

    ③ IOException
    当发生某种 I/O 异常时,抛出此异常。此类是失败或中断的 I/O 操作生成的异常的通用类。

    -EOFException
      当输入过程中意外到达文件或流的末尾时,抛出此异常。
    此异常主要被数据输入流用来表明到达流的末尾。
    注意:其他许多输入操作返回一个特殊值表示到达流的末尾,而不是抛出异常。
        
    -FileNotFoundException
      当试图打开指定路径名表示的文件失败时,抛出此异常。
    在不存在具有指定路径名的文件时,此异常将由 FileInputStream、FileOutputStream 和 RandomAccessFile 构造方法抛出。如果该文件存在,但是由于某些原因不可访问,比如试图打开一个只读文件进行写入,则此时这些构造方法仍然会抛出该异常。

    -MalformedURLException
      抛出这一异常指示出现了错误的 URL。或者在规范字符串中找不到任何合法协议,或者无法解析字符串。 
     
    -UnknownHostException
      指示主机 IP 地址无法确定而抛出的异常。

    ④ RuntimeException
       是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。可能在执行方法期间抛出但未被捕获的 RuntimeException 的任何子类都无需在 throws 子句中进行声明。
       Java编译器不会检查它。当程序中可能出现这类异常时,还是会编译通过。
       虽然Java编译器不会检查运行时异常,但是我们也可以通过throws进行声明抛出,也可以通过try-catch对它进行捕获处理。

    -ArithmeticException
    当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例。

    -ClassCastException
      当试图将对象强制转换为不是实例的子类时,抛出该异常。
    例如:Object x = new Integer(0);

    -LllegalArgumentException
      抛出的异常表明向方法传递了一个不合法或不正确的参数。

    -IllegalStateException
      在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。

    -IndexOutOfBoundsException 
      指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
    应用程序可以为这个类创建子类,以指示类似的异常。

    -NoSuchElementException
      由 Enumeration 的 nextElement 方法抛出,表明枚举中没有更多的元素。

    -NullPointerException
      当应用程序试图在需要对象的地方使用 null 时,抛出该异常。这种情况包括:
    调用 null 对象的实例方法。
    访问或修改 null 对象的字段。
    将 null 作为一个数组,获得其长度。
    将 null 作为一个数组,访问或修改其时间片。
    将 null 作为 Throwable 值抛出。
    应用程序应该抛出该类的实例,指示其他对 null 对象的非法使用。

    (二) SOF (堆栈溢出 StackOverflow)

    StackOverflowError 的定义:
    当应用程序递归太深而发生堆栈溢出时,抛出该错误。

    因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。

    栈溢出的原因:

    递归调用 大量循环或死循环 全局变量是否过多 数组、List、map数据过大

    (三)Android的OOM(Out Of Memory)

      当内存占有量超过了虚拟机的分配的最大值时就会产生内存溢出(VM里面分配不出更多的page)。
      
    一般出现情况:加载的图片太多或图片过大时、分配特大的数组、内存相应资源过多没有来不及释放。

    解决方法:
    ①在内存引用上做处理

    软引用是主要用于内存敏感的高速缓存。在jvm报告内存不足之前会清除所有的软引用,这样以来gc就有可能收集软可及的对象,可能解决内存吃紧问题,避免内存溢出。什么时候会被收集取决于gc的算法和gc运行时可用内存的大小。

     ②对图片做边界压缩,配合软引用使用
     
     ③显示的调用GC来回收内存 
     

    if(bitmapObject.isRecycled()==false) //如果没有回收 bitmapObject.recycle();

     ④优化Dalvik虚拟机的堆内存分配
      
      1.增强程序堆内存的处理效率
      

    floatTARGET_HEAP_UTILIZATION = 0.75f; VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);  2 .设置堆内存的大小 CWJ_HEAP_SIZE = 6* 1024* 1024 ; //设置最小heap内存为6MB大小 VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);

    ⑤ 用LruCache 和 AsyncTask<>解决

      从cache中去取Bitmap,如果取到Bitmap,就直接把这个Bitmap设置到ImageView上面。
      如果缓存中不存在,那么启动一个task去加载(可能从文件来,也可能从网络)。

    16 .Java面向对象的三个特征与含义

    对象
    是类的一个实例(对象不是找个女朋友),有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。


    是一个模板,它描述一类对象的行为和状态。

    1 . 封装性

      将对象的状态信息尽可能的隐藏在对象内部,只保留有限的接口和方法与外界进行交互,从而避免了外界对对象内部属性的破坏。

      Java中使用访问控制符来保护对类、变量、方法和构造方法的访问。
      Java支持4种不同的访问权限。 
      
      默认的,也称为default,在同一包内可见,不使用任何修饰符。
      私有的,以private修饰符指定,在同一类内可见。
      共有的,以public修饰符指定,对所有类可见。
      受保护的,以protected修饰符指定,对同一包内的类和所有子类可见。

    2. 继承

       java通过继承创建分等级层次的类,可以理解为一个对象从另一个对象获取属性的过程。
      
       类的继承是单一继承,也就是说,一个子类只能拥有一个父类
       下面的做法是不合法的:

    , Mammal{}

      但是我们可以用多继承接口来实现, 如:
      

    , Fruit2{}

      继承中最常使用的两个关键字是extends(用于基本类和抽象类)和implements(用于接口)。
      
       注意:子类拥有父类所有的成员变量,但对于父类private的成员变量却没有访问权限,这保障了父类的封装性。
       下面是使用关键字extends实现继承。
      

    { } { } { } { }

      通过使用关键字extends,子类可以继承父类所有的方法和属性,但是无法使用 private(私有) 的方法和属性。
    我们通过使用instanceof 操作符能够确定一个对象是另一个对象的一个分类。

    { (String args[]){ Animal a = new Animal(); Mammal m = new Mammal(); Dog d = new Dog(); System.out.println(m instanceof Animal); System.out.println(d instanceof Mammal); System.out.println(d instanceof Animal); } }

    结果如下:

    Implements关键字使用在类继承接口的情况下, 这种情况不能使用关键字extends。

    {} { } { }

    可以使用 instanceof 运算符来检验Mammal和dog对象是否是Animal类的一个实例。

    interface Animal{} {} { public static void main(String args[]){ Mammal m = new Mammal(); Dog d = new Dog(); System.out.println(m instanceof Animal); System.out.println(d instanceof Mammal); System.out.println(d instanceof Animal); } }

    运行结果如下:

    3.多态

      多态是同一个行为具有多个不同表现形式或形态的能力。
      多态性是对象多种表现形式的体现
      
      比如:我到宠物店说”请给我一只宠物”,服务员给我小猫、小狗或者蜥蜴都可以,我们就说”宠物”这个对象就具备多态性。 

    例子 {} {} {}

    因为Deer类具有多重继承,所以它具有多态性。
    访问一个对象的唯一方法就是通过引用型变量 (编译时变量)。
    引用型变量只能有一种类型,一旦被声明,引用型变量的类型就不能被改变了。
    引用型变量不仅能够被重置为其他对象,前提是这些对象没有被声明为final。还可以引用和它类型相同的或者相兼容的对象。它可以声明为类类型或者接口类型。

    Deer d = new Deer(); Animal a = d; Vegetarian v = d; Object o = d;

    所有的引用型变量d,a,v,o都指向堆中相同的Deer对象。

    我们来看下面这个例子:

    public class Animal { public String name = "父类name"; (){ System.out.println("父类move"); } (){ System.out.println("父类content"); } } { public String name = "子类name"; () { // TODO Auto-generated method stub System.out.println("子类move"); } (){ System.out.println("子类content"); } } public class Test { (String[] args) { Animal a = new Animal(); System.out.println(a.name); a.move(); a.content(); System.out.println("----------------------"); Animal b = new Bird(); //向上转型由系统自动完成 //编译时变量 运行时变量 System.out.println(b.name); b.move(); b.content(); System.out.println("----------------------"); Bird c = new Bird(); System.out.println(c.name); c.move(); c.content(); } }

    运行结果:

    父类name 父类move 父类content ---------------------- 父类name 子类move 子类content ---------------------- 子类name 子类move 子类content

    说明:Bird类继承Animal并重写了其方法。
       因为Animal b = new Bird(),编译时变量和运行时变量不一样,所以多态发生了。可以从最后的运行结果中看出,调用了父类的成员变量name和子类重写后的两个方法。
       上面继承说了,子类可以调用父类所有非private的方法和属性。因为name是一个String的对象,与方法不同,对象的域不具有多态性。通过引用变量来访问其包含的实例变量时,系统总是视图访问它编译时类型所定义的变量,而不是他运行时类型所定义的变量。

      那么问题来了,如果我们把Animal的成员变量换成private,那会不会去调用Bird类的成员变量name来打印输出呢?
      

    技术分享

    也就是说 系统访问的始终是去访问编译时类型所定义的变量。
       

      重写定义:子类对父类的允许访问的方法的实现过程进行重新编写!返回值和形参都不能改变。即外壳不变,核心重写!