Java 面试题(1)——java基础

3 篇文章 0 订阅

大家好,我是烤鸭:

    今天分享一些Java面试题和答案。

     这些答案都是自己想的,如果有理解不一样的,欢迎交流。

     

部分原题来自:

    https://blog.csdn.net/qq_41790443/article/details/80694415

 

1.    HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化

     首先说一下HashMap是什么,大部分人会说数组+链表,键值允许为null。

     我说说我的想法。HashMap内部类Node实现了Entry。四个属性:hash值,next节点地址值,key和value。

是存放在数组中。

     HashMap初始化的时候会创建一个默认大小为16的数组,默认负载因子0.75,当数组大小为16*0.75=12时,会发生扩容。

     JDK8 优化,HashMap数组初始化会在put方法时检查,如果数组为空,再去创建数组。JDK7 是初始化时都创建好了。

     putVal方法中的hash()方法是key的高16位异或低16位实现的比JDK7 更简单有效。

     JDK8 优化,相同hashcode的会接在链表的末尾。当链表长度大于8时,会变成红黑树。JDK7 一直是链表。

    JDK8 优化,resize()方法,遍历旧数组,oldTab[j]放入newTab中e.hash & (newCap - 1)的位置,链表重排时,原数组[j]位置上的桶移到了新数组[j+原数组长度]。JDK7 resize(),扩容后链表的顺序与原来相反。

    上面红色部分,移位运算:a % (2^n) 等价于 a & (2^n - 1),由于(hashCode中)新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了

    更多有关于HashMap的内容,推荐两篇博客:

    https://www.jianshu.com/p/17177c12f849

    https://tech.meituan.com/java_hashmap.html

 

2.    HaspMap扩容是怎样扩容的,为什么都是2的N次幂的大小。

       发生条件:当 数组中元素个数 超过 数组大小 * 负载因子时,就会发生扩容。

       过程简述(JDK 8 为例):源码就不贴了,只是简单说一下过程。

       如果原数组大小是0,创建新数组,初始化数组大小和负载因子以及,当前负载因子发生扩容时的数组大小。如果原数组已经超过数组的大小的最大值(2的30次方),就将数组大小置成Integer最大值(2的31次方 - 1)。 如果是 0 - 2的30次方 之间的,扩容2倍。

      遍历数组,判断是否是末节点,如果是的话,就把当前元素放到新数组[e.hash & (newCap - 1)]位置上(等价于a % (2^n))。如果不是,判断是否是红黑树节点,如果是树节点,遍历当前节点及后续节点,判断是否 (e.hash & bit == 0) (bit是原数组大小,添加节点时 e.hash & bit 完全是随机的,所以判断末尾是0还是1,来决定是高位还是低位节点)。不是树节点的情况(其实类似树节点),也是判断(e.hash & oldCap == 0),原数组[j]位置上的桶移到了新数组[j]。e.hash & oldCap != 0),原数组[j]位置上的桶移到了新数组[j+原数组长度]。

关于2的n次幂:a % (2^n) 等价于 a & (2^n - 1)。

HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

       

3.    HashMap,HashTable,ConcurrentHashMap的区别。

HashMap 允许一个NULL键和多个NULL值。非线程安全HashMap实现线程安全可以采用:Collections.synchronizedMap(map)
    HashTable 不允许NULL键和NULL值。线程安全使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占。
    ConcurrentHashMap(jdk7) 允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
    (jdk8) 摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用SynchronizedCAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。

     并发的多线程使用场景中使用HashMap可能造成死循环(jdk7),HashTable效率太低不适合在高并发情况下使用,应该使用线程安全的ConcurrentHashMap。

      https://www.cnblogs.com/zq-boke/p/8654539.html

 

4.    极高并发下HashTable和ConcurrentHashMap哪个性能更好,为什么,如何实现的。

    ConcurrentHashMap 性能更好。
    jdk 1.8以后,HashTable已经淘汰了,并发时使用一把锁处理并发问题,当有多个线程访问时,需要多个线程竞争一把锁,导致阻塞
    jdk 1.7 ConcurrentHashMap则使用分段,相当于把一个HashMap分成多个,然后每个Segment分配一把锁,这样就可以支持多线程访问。
    jdk 1.8 ConcurrentHashMap取消segments字段,直接采用transient volatile HashEntry<K,V> table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
    并发控制使用Synchronized和CAS来操作。JDK8中的实现也是锁分离思想,只是锁住的是一个node,而不是JDK7中的Segment;锁住Node之前的操作是基于在volatile和CAS之上无锁并且线程安全的。

 

5.    HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。

    jdk1.7 在并发的多线程使用场景中使用HashMap可能造成死循环,put过程中的resize方法在调用transfer方法的时候导致的死锁。
    jdk1.8 会将原来的链表结构保存在节点e中,然后依次遍历e,根据hash&n是否等于0,分成两条支链,保存在新数组中。
    但是有可能出现数据丢失的情况。

 

6.    java中四种修饰符的限制范围。

private 本类         default 本包下其他类          protected 不同包下子类            public 不同包下非子类

7.    Object类中的方法

        equals
        hashCode
        toString
        getClass
        notify
        notifyAll
        wait * 3

8.    接口和抽象类的区别,注意JDK8的接口可以有实现。

    抽象类,子类继承,关系是“是一种”。接口,子类实现,关系是“有一种”。
    以jdk 1.8为例:
    抽象类的方法可以声明default修饰方法,interface只能用public static修饰。
    抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
    接口中不能含有静态代码块,而抽象类可以有静态代码块。
    一个类只能继承一个抽象类,而一个类却可以实现多个接口。

 

9.    动态代理的两种方式,以及区别。

    JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
    CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

    区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。
    1. JDK代理使用的是反射机制实现aop的动态代理,CGLIB代理使用字节码处理框架asm,通过修改字节码生成子类。
    所以jdk动态代理的方式创建代理对象效率较高,执行效率较低,cglib创建效率较低,执行效率高
    2. JDK动态代理机制是委托机制,具体说动态实现接口类,在动态生成的实现类里面委托hanlder去调用原始实现类方法。
    CGLIB则使用的继承机制,具体说被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。

    https://blog.csdn.net/weixin_36759405/article/details/82770422

 

10.    Java序列化的方式。

    序列化就是把Java对象储存在某一地方(硬盘、网络),也就是将对象的内容进行流化(二进制)。
    Java Serialization(主要是采用JDK自带的Java序列化实现,性能很不理想)
    Json(目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,还有谷歌的Gson)
    Hession(它基于HTTP协议传输,使用Hessian二进制序列化,对于数据包比较大的情况比较友好。)
    Dubbo Serialization(阿里dubbo序列化)
    FST(高性能、序列化速度大概是JDK的4-10倍,大小是JDK大小的1/3左右)
    Protocol Buffer(Google出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json、XML 强)
    kryo(比kyro更高效的序列化库就只有google的protobuf了)

    关于kryo,https://blog.csdn.net/eguid_1/article/details/79316403

 

11.    传值和传引用的区别,Java是怎么样的,有没有传值引用。

    java函数中的参数都是传递值的,所不同的是对于基本数据类型传递的是参数的一份拷贝,对于类类型传递的是该类参数的引用的拷贝。
    当在函数体中修改参数值时,无论是基本类型的参数还是引用类型的参数,修改的只是该参数的拷贝,不影响函数实参的值,如果修改的是引用类型的成员值,则该实参引用的成员值是可以改变的

https://www.cnblogs.com/zhangj95/p/4184180.html

 

12.    一个ArrayList在循环过程中删除,会不会出问题,为什么。

    这里只考虑单线程的问题,ArrayList线程不安全。
    循环方式不同,结果不同:
    增强for循环,它对索引的边界值只会计算一次,使用list的remove方法会改变modCount值,校验和expectedModCount不一            样,有可能抛出ConcurrentModificationException
    普通for循环,由于remove方法会调用System.arraycopy,改变数组元素排序,有可能抛出IndexOutOfBoundsException
    iterator循环,iterator.remove()方法来移除元素是没有问题的。迭代器的remove方法每次会expectedModCount值改成modCount。

参考:https://www.cnblogs.com/hupu-jr/p/7891844.html

 

13.    @Transactional注解在什么情况下会失效,为什么。

    方法不是public的

    异常类型是不是unchecked异常(解决方案:rollbackFor=Exception.class)

    数据库引擎要支持事务,如果是MySQL,注意表要使用支持事务的引擎,比如innodb,如果是myisam,事务是不起作用的

    是否开启了对注解的解析

    spring是否扫描这个包

    同一个类中的方法调用

    异常被catch

14.        List 和 Set 的区别


都是实现Collection接口的。
List:1.可以允许重复的对象。
    2.可以插入多个null元素。
         3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
         4.常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
Set:1.不允许重复对象
    2. 无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator  或者 Comparable 维护了一个排序顺序。
         3. 只允许一个 null 元素
         4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。
详细的解释:
https://www.cnblogs.com/IvesHe/p/6108933.html

 

15.        HashSet 是如何保证不重复的


    HashSet内部是HashMap,调用set的add方法就是调用map的put方法
    我们知道HashMap的key是不重复的。
    根据hashCode和equals判断是否重复。

16.        Java反射机制?功能?实际使用?


    JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
    对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
    部分信息是source阶段不清晰,需要在runtime阶段动态临时加载。
    获取class对象:
    1 ClassName.getClass()  2 ClassName.class 3 Class.forName(String className);
    获取构造方法和创建实例:
    clazz.getConstructors()(构造方法)
    constructor.newInstance()(创建对象)。
    获取共有或私有方法:
    clazz.getDeclaredMethod获取方法,invoke(newInstance,"")调用方法。
    获取注解:
    clazz.getAnnotations();
    最常见应用:
    利用反射获取配置文件内容。
    对象转Map,对象转json。
    文件复制。
    spring(IOC 和 aop),Hibernate 和mybatis 很多框架用到了。
    反射可以跳过泛型检查。


    https://blog.csdn.net/yongjian1092/article/details/7364451


17.        Arrays.sort 实现原理和 Collection 实现原理

当调用Arrays.sort(Object[] objects)时,先调用的是归并的sort方法。
对于归并排序的改进
    以上方法对给定数组的指定区间内的数据进行排序,同时允许调用者提供用于归并排序的辅助空间。
    实现思路为:首先检查数组的大小,如果数组比较小(286),则直接调用改进后的快速排序完成排序
    如果数组较大,则评估数组的无序程度,如果这个数组几乎是无序的,那么同样调用改进后的快速排序算法排序
    如果数组基本有序,那么采用归并排序算法对数组进行排序。
·对于快速排序的改进: 
    该算法的实现了一种称为“DualPivotQuicksort”的排序算法,中文可以翻译为“双枢轴快速排序”,可以看作是经典快速排序算法的变体。
    算法的基本思路是:如果数组的数据量较少(47),则执行插入排序就可以达到很好的效果,如果数据量较大,
    那么确定数组的5个分位点,选择一个或两个分位点作为“枢轴”,然后根据快速排序的思想进行排序。

Collections.sort时:
调用的也是Arrays.sort,再将排好序的值set进去。
Array.sort调用的是TimSort,TimSort算法是一种起源于归并排序和插入排序的混合排序算法。
如果数组小于MIN_MERGE(32),则调用binarySort,使用二分查找的方法将后续的数插入之前的已排序数组
大于MIN_MERGE:
选取minRun(数组长度右移,直到小于 MIN_MERGE)。
找到初始升序序列,如果降序,会对其翻转
若这组区块大小小于minRun,则将后续的数补足,利用binarySort 对run 进行扩展。
入栈需要合并的数组
合并数组,重复以上的步骤。

Arrays.sort :
https://blog.csdn.net/octopusflying/article/details/52388012
Collections.sort:
https://blog.csdn.net/bruce_6/article/details/38299199
https://www.jianshu.com/p/1efc3aa1507b


18.        LinkedHashMap的应用

HashMap是无序的,LinkedHashMap是有序的,且默认为插入顺序。
LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的。
HashMap无序;LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)。
LinkedHashMap存取数据,还是跟HashMap一样使用的Entry[]的方式,双向链表只是为了保证顺序。
LinkedHashMap是线程不安全的。


https://www.jianshu.com/p/8f4f58b4b8ab


19.        cloneable接口实现原理

实现clone接口的类,调用clone方法,属于深拷贝。
复制出来的对象属于不同的地址,改变复制对象的属性值不会影响原对象。
浅拷贝的话,拷贝的对象和原有的对象指向同一个地址值,改变其中的属性值,
另一个也会改变。

https://blog.csdn.net/u013916933/article/details/51590332

20.        数组在内存中如何分配

数组引用变量是存放在栈内存(stack)中,数组元素是存放在堆内存(heap)中。
数组初始化分为静态初始化(在定义时就指定数组元素的值,此时不能指定数组长度,否则就出现了静态加动态混搭初始化数组了)
动态初始化(只指定数组长度,由系统分配初始值,初始值根据定义的数据类型来)。
堆中变量没有引用会等待垃圾回收。
栈中变量会在脱离作用域后释放。


https://blog.csdn.net/lcl19970203/article/details/54428358
https://www.cnblogs.com/duanxz/p/6102583.html

21.        BlockingQueue的使用及实现

利用 LinkedBlockingQueue 实现生产者和消费者队列,伪代码如下:

//消费者
class Consumer implements Runnable{
	private BlockingQueue<String> queue;
	 public Consumer(BlockingQueue<String> queue) {
        this.queue = queue;
    }
    public void run() {
        while (true) {
            String data = queue.poll(2, TimeUnit.SECONDS);
        }
    }
}
//生产者
class Producer implements Runnable{
  	private BlockingQueue queue;
	public Producer(BlockingQueue queue) {
        this.queue = queue;
    }
    public void run() {
        while (true) {
        	//延迟2s入数据
            queue.offer(data, 2, TimeUnit.SECONDS);
        }
    }
}
//测试类
TestBlockingQueue{
	@Test
	public void test(){
		BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);
        Producer producer1 = new Producer(queue);
        Producer producer2 = new Producer(queue);
        Producer producer3 = new Producer(queue);
        Consumer consumer = new Consumer(queue);
        // 借助Executors
        ExecutorService service = Executors.newCachedThreadPool();
        // 启动线程
        service.execute(producer1);
        service.execute(producer2);
        service.execute(producer3);
        service.execute(consumer);
	}
}

http://www.cnblogs.com/jackyuj/archive/2010/11/24/1886553.html
 

22.        BIO、NIO、AIO区别

Blocking IO(同步阻塞)
面向流,阻塞。

Non-Blocking IO(同步非阻塞)
面向块(buffer),非阻塞。
非阻塞IO模型:
如果没有数据,立即返回EWOULDBLOCK。
用户线程不断轮询(polling)内核,是否有数据。
有数据后,由OS拷贝数据到内核缓冲区,再由内核缓冲区拷贝到用户线程缓冲区。

IO复用模型:
(select + pool 无差别的轮询方式)
多个I/O的阻塞复用到同一个select阻塞上。
由多路复用器不断轮询(poll) I/O线程,看看是否有数据。
如果没有数据,就把当前线程阻塞,如果有数据,就唤醒,轮询一遍所有的流。

(epool 最小轮询方式)
通过epoll方式来观察多个流,epoll只会把发生了I/O事件的流通知我们。
时间复杂度降低为O(k),k为发生事件的流的个数。
如果所有的IO都是短连接且事件发生的比较快,epoll和select + poll效率差不多。

epoll相比于select/poll的优势:
监视的描述符数量不受限制,所支持的FD上限是最大可以打开文件的数目;
I/O效率不会随着监视fd的数量增长而下降。
epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。

信号驱动模型:
类似epoll,需要开启Socket的信号驱动式I/O功能,通过sigaction系统调用来安装一个信号处理函数。
当有数据的时候,内核就为该进程产生一个SIGIO信号,通过该信号的值做处理。

Asynchronous IO
收到用户请求,立刻返回,不会阻塞用户进程。
等待数据完成后,将数据拷贝到用户内存,然后发出一个信号。

https://blog.csdn.net/historyasamirror/article/details/5778378
https://www.jianshu.com/p/db5da880154a
https://www.jianshu.com/p/439e8b349f48

23.        事务隔离级别 和 事务传播级别

五大隔离级别:
ISOLATION_DEFAULT(默认级别)
ISOLATION_READ_UNCOMMITTED(读未提交,可能导致脏读、幻读、重复读)
ISOLATION_READ_COMMITTED (读已提交,可能导致幻读、重复读)
ISOLATION_REPEATABLE_READ(不可重复读,可能导致幻读)
ISOLATION_SERIALIZABLE (通过锁表避免以上情况)

七个传播级别:
PROPAGATION_REQUIRED:如果没有事务就新建一个。
PROPAGATION_SUPPORTS:按当前事务执行,如果当前没有事务,就按没事务的方式执行。
PROPAGATION_MANDATORY:表示该方法必须运行在一个事务中。如果当前没有事务正在发生,将抛出一个异常。
PROPAGATION_REQUIRES_NEW:创建新的事务,如果存在事务,旧的事务会挂起。
PROPAGATION_NOT_SUPPORTED:以无事务的方式运行。
PROPAGATION_NESTED:    如果已有事务,就嵌套事务。没有的话,按 PROPAGATION_REQUIRED 处理。
ISOLATION_DEFAULT:使用数据库的默认隔离级别。
 

24.   http的七层协议

网络七层协议由下往上分别为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
TCP/IP五层模型:应用层、传输层、网络层、数据链路层和物理层。


https://blog.csdn.net/a5582ddff/article/details/77731537

25.    ArrayList怎么实现扩容


新的容量,原来容量的1.5倍。
调用Arrays.copyOf()。

https://blog.csdn.net/eases_stone/article/details/79843851

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烤鸭的世界我们不懂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值