Java基础面试题

1,529 阅读15分钟

重载和重写的区别?

  1. 重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。   
  2. 重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。

String 为什么是不可变的?

简单的来说:String 类中使用 final 关键字字符数组保存字符串,所以 String 对象是不可变的

private final char value[]

String StringBuffer 和 StringBuilder 的区别是什么?

  1. 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。
  2. StringBuffer底层使用synchronized对方法进行修饰,所以是线程安全的,但性能会比StringBuilder稍低,而StringBuilder未加锁,所以是线程不安全的

对于三者使用的总结:

  1. 操作少量的数据 = String
  2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据 = StringBuffer

在一个静态方法内调用一个非静态成员为什么是非法的

因为静态方法是属于类的,而非静态成员是属于实例对象的,类不能操作实例对象,而实例对象可以操作类,就像非静态方法可以调用静态方法

接口和抽象类的区别是什么

  1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法
  2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
  3. 一个类可以实现多个接口,但最多只能实现一个抽象类
  4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定

成员变量与局部变量的区别有那些

  1. 从语法形式上,看成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所
  2. 修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰;
  3. 从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存
  4. 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  5. 成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外被 final 修饰的成员变量也必须显示地赋值);而局部变量则不会自动赋值。

== 与 equals的区别

  1. == :它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
  2. equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
  • 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

举个例子

public class test1 { 
    public static void main(String[] args) {     
        String a = new String("ab"); // a 为一个引用 
        String b = new String("ab"); // b为另一个引用,对象的内容一样 
        String aa = "ab"; // 放在常量池中        
        String bb = "ab"; // 从常量池中查找       
        if (aa == bb) // true 
            System.out.println("aa==bb");       
            if (a == b) // false,非同一对象   
            System.out.println("a==b");      
            if (a.equals(b)) // true      
            System.out.println("aEQb");        
            if (42 == 42.0) { // true      
            System.out.println("true"); 
        } 
    } 
} 

说明:

  • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

hashCode()与 equals()的相关规定

  1. 如果两个对象相等,则 hashcode 一定也是相同的
  2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true
  3. 两个对象有相同的 hashcode 值,它们也不一定是相等的
  4. 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
  5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

Java 序列化中如果有些字段不想进行序列化 怎么办

对于不想进行序列化的变量,使用 transient 关键字修饰。

Java 异常类层次结构图

2

finally不会被执行的特殊情况

  1. 在 finally 语句块中发生了异常。
  2. 在前面的代码中用了 System.exit()退出程序。
  3. 程序所在的线程死亡。
  4. 关闭 CPU。

Java的四种引用,强弱软虚

  1. 强引用:强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收。

    String str = new String("str");
    System.out.println(str);
    
  2. 软引用:软引用在程序内存不足时,会被回收。

    // 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
    // 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
    SoftReference<String> wrf = new SoftReference<String>(new String("str"));
    
  3. 弱引用:弱引用就是只要JVM垃圾回收器发现了它,就会将之回收, 可用场景: Java源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用,我的理解就是, 一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。

    WeakReference<String> wrf = new WeakReference<String>(str)
    
  4. 虚引用:虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意 哦,其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多 被用于引用销毁前的处理工作。

    PhantomReference<String> prf = new PhantomReference<String>(new String("str"),new ReferenceQueue<>())
    

深拷贝和浅拷贝的区别是什么?

  1. 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指 向原来的对象.换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象.
  2. 深拷贝:被复制对象的所有变量都含有与原来的对象相同的值.而那些引用其他对象的变量将指向 被复制过的新对象.而不再是原有的那些被引用的对象.换言之.深拷贝把要复制的对象所引用的 对象都复制了一遍.

获取一个类Class对象的方式有哪些?

  1. 通过类对象的 getClass() 方法获取,细心点的都知道,这个 getClass 是 Object 类里面的 方法。

    User user=new User();
    //clazz就是一个User的类对象
    Class<?> clazz=user.getClass()
    
  2. 通过类的静态成员表示,每个类都有隐含的静态成员 class。

    //clazz就是一个User的类对象
    Class<?> clazz=User.class;
    
  3. 通过 Class 类的静态方法 forName() 方法获取。

    Class<?> clazz = Class.forName("com.tian.User");
    

ArrayList和linkedList的区别

  1. ArrayList的底层数据结构为Object数组,所以基于数组的特性获取数据的时间复杂度是O(1),但是要删除数据却是开销很大,因为这需要重排数组中的所有 数据
  2. LinkList是一个双链表,所以在查询数据的时间复杂度为O(n),而在添加和删除元素时具有比ArrayList更好的性能

HashMap和HashTable的区别

  1. HashMap是线程是线程不安全,而HashTable是线程安全的,因为HashTable底层方法上用了synchronized进行修饰,但HashTable的性能比HashMap要低
  2. Hashtable:key和value都不能为null。 HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个 key值对应的value为null。

什么是 fail-fast

fail-fast 机制是 Java 集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行 操作时,就可能会产生 fail-fast 事件。

例如:当某一个线程 A 通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变 了,那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事 件。这里的操作主要是指 add、remove 和 clear,对集合元素个数进行修改。 解决办法:建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。 可以这么理解:在遍历之前,把 modCount 记下来 expectModCount,后面 expectModCount 去 和 modCount 进行比较,如果不相等了,证明已并发了,被修改了,于是抛出 ConcurrentModificationException 异常。

HashMap 的长度为什么是 2 的 N 次方呢

因为计算哈希值的时候是使用取模运算,取模(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说 hash % length == hash & (length - 1) 的前提是 length 是 2 的 n 次方),并且,采用二进 制位操作 & ,相对于 % 能够提高运算效率,那么他就需要把HashMap内部的数组长度固定为 2^n 的长度了,也就是说HashMap里面的数组的长度,始终都是2的n次幂。

这就是为什么 HashMap 的长度需要 2 的 N 次方了。

HashMap 与 ConcurrentHashMap 的异同

  1. 都是 key-value 形式的存储数据;
  2. HashMap 是线程不安全的,ConcurrentHashMap 是 JUC 下的线程安全的;
  3. HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。JDK 1.8 之后是数组 + 链表 + 红黑 树。当链表中元素个数达到 8 的,并且数组长度大于64的时候,链表会转为红黑树,因为红黑树查询速度快;
  4. HashMap 初始数组大小为 16(默认),当出现扩容的时候,以 0.75 * 数组大小的方式进行扩 容;
  5. ConcurrentHashMap 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry, Segment 数组大小默认是 16,2 的 n 次方;JDK 1.8 之后,采用 Node + CAS + Synchronized 来保证并发安全进行实现。

红黑树有哪几个特征

  1. 每个节点是红色或者黑色
  2. 根节点是黑色
  3. 每个叶子节点都是黑色(指向空的叶子节点)
  4. 如果一个叶子节点是红色,那么其子节点必须是黑色
  5. 从一个节点到该节点的子孙节点的所有路劲上包含相同数目的黑节点

ArrayList扩容机制

在添加元素的时候,当元素的个数>数组的长度时,会出发ArrayList的扩容,扩容最后的长度是:当前数组长度+当前数组长度向左移动一位

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

HashMap的扩容机制

在HashMap中有两个比较重要的属性,一个是初始化的大小(默认16),一个是负载因子(默认0.75),他们两个决定了HashMap真正能存多少个元素,当HashMap中实际存储的元素个数超过其容量时,会触发Hash的扩容机制,也就是resize方法。

因为HashMap的扩容是2的n次方,所以在扩容之后节点的位置可能有两种变化:

  • 保持原位置不动(新bit位为0时)
  • 散列原索引+扩容大小的位置去(新bit位为1时)

比如当数组长度从16到32,其实只是多了一个bit位的运算,我们只需要在意那个多出来的bit为是0还是1,是0的话索引不变,是1的话索引变为当前索引值+扩容的长度,比如5变成5+16=21,这样的扩容方式不仅节省了重新计算hash的时间,而且保证了当前桶中的元素总数一定小于等于原来桶中的元素数量,避免了更严重的hash冲突,均匀的把之前冲突的节点分散到新的桶中去

  final Node<K,V>[] resize() {
        //oldTab:引用扩容前的哈希表
        Node<K,V>[] oldTab = table;
        //oldCap:表示扩容前的table数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //获得旧哈希表的扩容阈值
        int oldThr = threshold;
        //newCap:扩容之后table数组大小
        //newThr:扩容之后下次触发扩容的条件
        int newCap, newThr = 0;
        //条件成立说明hashMap中的散列表已经初始化过了,是一次正常扩容
        if (oldCap > 0) {
            //判断旧的容量是否大于等于最大容量,如果是,则无法扩容,并且设置扩容条件为int最大值,
            //这种情况属于非常少数的情况
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }//设置newCap新容量为oldCap旧容量的二倍(<<1),并且<最大容量,而且>=16,则新阈值等于旧阈值的两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //如果oldCap=0并且边界值大于0,说明散列表是null,但此时oldThr>0
        //说明此时hashMap的创建是通过指定的构造方法创建的,新容量直接等于阈值
        //1.new HashMap(intitCap,loadFactor)
        //2.new HashMap(initCap)
        //3.new HashMap(map)
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //这种情况下oldThr=0;oldCap=0,说明没经过初始化,创建hashMap
        //的时候是通过new HashMap()的方式创建的
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //newThr为0时,通过newCap和loadFactor计算出一个newThr
        if (newThr == 0) {
            //容量*0.75
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
                //根据上面计算出的结果创建一个更长更大的数组
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //将table指向新创建的数组
        table = newTab;
        //本次扩容之前table不为null
        if (oldTab != null) {
            //对数组中的元素进行遍历
            for (int j = 0; j < oldCap; ++j) {
                //设置e为当前node节点
                Node<K,V> e;
                //当前桶位数据不为空,但不能知道里面是单个元素,还是链表或红黑树,
                //e = oldTab[j],先用e记录下当前元素
                if ((e = oldTab[j]) != null) {
                    //将老数组j桶位置为空,方便回收
                    oldTab[j] = null;
                    //如果e节点不存在下一个节点,说明e是单个元素,则直接放置在新数组的桶位
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果e是树节点,证明该节点处于红黑树中
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //e为链表节点,则对链表进行遍历
                    else { // preserve order
                        //低位链表:存放在扩容之后的数组的下标位置,与当前数组下标位置一致
                        //loHead:低位链表头节点
                        //loTail低位链表尾节点
                        Node<K,V> loHead = null, loTail = null;
                        //高位链表,存放扩容之后的数组的下标位置,=原索引+扩容之前数组容量
                        //hiHead:高位链表头节点
                        //hiTail:高位链表尾节点
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //oldCap为16:10000,与e.hsah做&运算可以得到高位为1还是0
                            //高位为0,放在低位链表
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    //loHead指向e
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //高位为1,放在高位链表
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //低位链表已成,将头节点loHead指向在原位
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //高位链表已成,将头节点指向新索引
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

计划用HashMap存1k条数据,构造时传1000会触发扩容吗

  • HashMap 初始容量指定为 1000,会被 tableSizeFor() 调整为 1024;
  • 但是它只是表示 table 数组为 1024;
  • 负载因子是0.75,扩容阈值会在 resize() 中调整为 768(1024 * 0.75)
  • 会触发扩容

计划用HashMap存1w条数据,构造时传10000会触发扩容吗

  • 当我们构造HashMap时,参数传入进来 1w
  • 经过 tableSizeFor() 方法处理之后,就会变成 2 的 14 次幂 16384
  • 负载因子是 0.75f,可存储的数据容量是 12288(16384 * 0.75f)
  • 完全够用,不会触发扩容
 static int tableSizeFor(int cap) {
     int n = cap - 1;
     n |= n >>> 1;
     n |= n >>> 2;
     n |= n >>> 4;
     n |= n >>> 8;
     n |= n >>> 16;
     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
 }

HashMap头插法会导致死循环问题

  • hashmap1.7中的死循环是有多个线程并发扩容形成了环状链表,随后再进行扩容的线程会循环取这个环状链表的节点,造成死循环;其次,环状链表是几个节点相互指向,并不是某个节点自己指向自己。
  • HashMap1.8中虽然尾插法会解决死循环的问题,但是多线程情况下还是会导致数据丢失的问题, 建议多线程环境下使用线程安全的concurrentHashMap

详细解释地址:blog.csdn.net/weixin_4663…

ConcurrentHashMap是怎样保证线程安全的

  • 在 JDK1.7 的时候,ConcurrentHashMap 底层采⽤ 分段的数组+链表实现, ConcurrentHashMap 使用分段锁对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。
  • 到了 JDK1.8 的时候已经 摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发 控制使⽤ synchronized 和 CAS 来操作。

HashSet如何检查重复

当你把对象加⼊ HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,同 时也会与其他加⼊的对象的 hashcode 值作⽐,如果没有相符的 hashcode , HashSet 会假设 对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查 hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让加⼊操作成功。

⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

  1. HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储null 值;
  2. LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
  3. TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排序

面试题下载

gh_57270b48e2cc_258.jpg