面试Java基础问题汇总 part1

编译时多态、运行时多态

c++要更复杂,Java相对而言更容易回答。
多态按执行过程分为两种情况,编译时多态和运行时多态。

运行时多态的概念也可以被说成“一个接口,多个方法”。

方法重载都是编译时多态。根据参数列表(数据类型、个数和次序),Java在编译时能够确定执行重载方法的哪一个。

方法覆盖会表现出两种不同的多态性,当对象引用本类实例时,为编译时多态,否则(例:父类对象引用子类实例)则为运行时多态。

在性能要求较高的代码中不提倡运用运行时多态,运行时多态方法较普通方法而言系统开销更大。

 

补充:泛型也是多态性的一种体现,是编译时多态。

equals()

==就不介绍了,它永远比较值。基础数据类型比较大小,引用数据类型比较地址值是否相同。

equals()判断两个对象是否相等:

  1. 类没有覆盖equals方法,则等价于通过"=="比较对象。
  2. 类覆盖了equal是方法。一般会去比较两个对象的内容是否相等。

Java String equals方法实例

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

即先比较两个对象地址是否相等,如果地址相等,则他们的内容一定相等;如果地址不相等,会比较String的内容是否相等,比较方法就是遍历存储string的value数组(char类型,JDK9变为byte类型)。

hashCode()

hashCode()函数的作用是获取散列码,它只在散列表中有用,在其他情况下没用。在散列表中,hashCode() 的作⽤是获取对象的散列码,进⽽确定该对象在散列表中的位置。如果两个对象的hashcode不相等,则它们一定不相等;如果hashcode相等,两个对象也不一定相等(hash collision),再调用equals方法判断两者是否相等。这样可以减少equals()的调用次数,大大提高执行速度。

下面是String类里面的hashCode()函数。

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

线程基本状态

Java线程只有这六种状态。

  • NEW
  • RUNNABLE (就绪和运行统称)
  • BLOCKED
  • WAITING
  • TIME_WAITING
  • TERMINATED

Java IO

详细请去其他地方看,这里只总结一些规律。

特点划分
Reader/Writer结尾字符流
Stream结尾字节流

BIO\NIO\AIO (重点、必考)

再去其他地方看看,面试只懂下面这些肯定不行。  

BIO(Blocking I/O): 同步阻塞I/O模型,数据的读取写入必须阻塞在一个线程内等待其完成。在连接数不是很高的情况下,还是不错的,每一个连接专注于自己的I/O并且编程模型简单,不用考虑系统的过载,限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接请求。但是,十万级/百万级连接的时候,传统的BIO模型是无能为力的。

NIO(Non-blocking/New I/O): 同步非阻塞I/O模型,在Java1.4中引入了NIO框架,对应java.nio包,提供了Channel, Selector, Buffer等抽象。它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了与传统BIO模型中的SocketSeverSocket相对应的SocketChannelServerSocketChannel两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式,阻塞模型使用就像传统中的支持一样,简单,但是性能和可靠性都不好;非阻塞模型刚好相反。

对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式开发。

AIO(Asynchronous I/O): AIO即NIO2。于Java 7引入,它是异步非阻塞的IO模型。异步IO基于事件和回调机制实现,也就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知线程进行后续的操作。

深拷贝 vs 浅拷贝

对于基本数据类型来讲,都是值传递。

对引用数据来讲,对于引用值进行传递的拷贝,为浅拷贝;创建新的对象,复制其内容,返回新对象的地址,为深拷贝。

C++、Python都有这个概念。

import copy
nums = [1, 2, 3, 4]
c1 = copy.copy(nums) # 浅拷贝
c2 = copy.deepcopy(nums) # 深拷贝

ArrayList扩容

初始化ArrayList,不加参数的时候,默认生成了一个空数组。

加入第一个元素的时候,add()方法首先调用ensureCapacityInternal(size + 1),将minCapacity设置为传入参数和默认容量(10),此时minCapacity = 10。

然后调用ensureExplicitCapacity方法。

 //判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

加入第一个元素时,minCapacity -elementData.length = 10- 0 = 10 > 0,调用grow()扩容。

    /**
     * 要分配的最大数组大小
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * ArrayList扩容的核心方法。
     */
    private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
       // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
       //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
        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);
    }

newCapacity(1) - minCapacity(10) < 0,newCapacity被置为10。

下次容量超过10的时候再次扩容,扩容之后为原来的.大约1.5倍,偶数正好,奇数抹去了小数部分。

HashMap和HashTable的区别

HashTable已经基本被淘汰,建议使用CocurrentHashMap。

  1. HashMap是非线程安全的,HashTable是线程安全的。HashTable内部的方法基本都经过synchronized修饰。
  2. HashMap的效率更好(因为不需要考虑线程安全)。
  3. HashMap,null可以作为键,这样的键只有一个,可以有一个或者多个键所对应的值为null。HashTable中如果put进null作键值,会报NullPointerException
  4. 初始容量大小和每次扩充容量大小的不同:(1) 创建时如果不指定容量初始值,HashTable默认初始大小为11,每次扩充变为原来的2n+1。HashMap默认的初始化大小为16,每次扩容变为原来2倍。
    (2) 创建时如果给定了容量的初始值,那么HashTable会直接使用你给的大小,而HashMap会将其扩充为2的幂次方大小(HashMap中的tableSizeFor()方法保证,HashMap总是使用2的幂作为哈希表的大小,这是因为indexFor方法使用(h&(length-1))来决定索引位置,是因为length是2的幂次方时,&运算接近%运算,能够均分,不会冲突。
  5. 底层数据结构:JDK1.8以后HashMap解决哈希冲突时,链表长度大于阀值(默认为8),会将链表转化为红黑树,以减少搜索时间。HashTable没有这个机制。
hash % length == hash & (length-1)

该等式仅在length是2的幂的时候成立,使用&运算比%运算效率高得多。

HashMap和HashSet的区别

HashSet底层基于HashMap实现。HashSet的源码中,除了clone()writeObject()readObject()是HashSet自己不得不实现之外,其他都是直接调用HashMap中的方法。

HashMapHashSet
实现了Map接口实现了Set接口
存储键值对仅存储对象
put()添加元素add()添加元素
HashMap使用键(Key)计算hashcode。HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以使用equals()方法来判断对象的相等性。

HashMap底层实现(重要)

JDK1.8之前HashMap底层是数组和链表结合在一起(链表散列)。HashMap经过扰动函数处理后得到hash值,然后通过(n-1) & hash判断当前元素存放的位置。如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同。如果相同的话,直接覆盖,不相同就使用拉链法解决冲突。

扰动函数,使用key.hashCode()得到h之后再与h>>>16取异或,目的是为了尽可能减少碰撞。

ConcurrentHashMap和HashTable的区别(重要)

主要在于实现线程安全的方式上不同。

底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组+链表。JDK1.8采用的数据结构跟HashMap相同,都是数组+链表/红黑树。HashTable是数组加链表。

实现线程安全的方式:
(1) JDK1.7,ConcurrentHashMap使用分段锁对整个桶数组进行了分割分段(segment)每一把锁只锁容器的其中一部分数据,多线程访问容器里不同分段的数据,就不存在锁竞争,提高并发效率。JDK1.8时已经摒弃了segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现。并发控制使用synchronized和CAS来操作。

CAS:Compare and Swap,即比较再交换。
CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

(2) HashTable使用synchronized来保证线程安全(同一把锁)。这种方式效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态。

详见这里

参考资料

JavaGuide面试突击版,百度可得最新版。
编译时多态、运行时多态

posted @ 2020-09-24 09:53  从流域到海域  阅读(48)  评论(0编辑  收藏  举报