java-常用面试题

技术文档网 2021-04-16

谈谈你对Java平台的理解?Java是解释执行,这句话对吗?

Java本身是一种面向对象的语言,最显著的特性有两个方面: 一、所谓的书写一次,到处运行,能够非常容易地获得跨平台能力; 二、垃圾收集,Java通过垃圾收集器回收分配内存,大部分情况下,程序员不需要自己操心内存的分配和回收。 三、Java语言特性,包括泛型,Lambda等语言特性; 四、基础类库:集合、IO/NIO、网络、并发、安全等基础类库。 五、Java的类加载机制:内嵌的Class-Loader【Bootstrap、Application和Extension Class-Loader】。 六、类加载大致过程:加载、验证、链接、初始化。

Javac的编译,编译Java源码生成的.class文件里面实际是字节码,而不是可以直接执行的机器码。JDK 8 实际是解释和编译混合的一种模式,即所谓的混合模式。我们开发的Java的源代码,首先通过Javac编译成字节码,然后,在运行时,通过Java虚拟机内嵌的解释器将字节码转换为最终的机器码。但是常见的JVM都提供了JIT(Just In Time)编译器,也就是所谓的动态编译器,JIT能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于编译执行,而不是解释执行了。

Exception和Error有什么区别?另外,运行时异常与一般异常有什么区别?

Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出【throw】或者捕获【catch】,它是异常处理机制的基本组成类型。 Exception和Error体现了Java平台设计者对不同异常情况的分类。 Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。 Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序处于非正常、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类。 Exception又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译器检查的一部分;不检查异常就是所谓的运行时异常,类似NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译器强制要求。

静态内部类与普通内部类

1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象的引用;

2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为1中添加的成员变量赋值;

3 在调用内部类的构造函数初始化内部类对象时, 会默认传入外部类的引用。【内部类对象的创建依赖于外部类对象。】

非静态内部类、非静态匿名内部类会持有外部对象的引用,静态内部类、静态匿名内部类则不会持有外部对象的引用。

静态内部类虽然也定义在外部类的里面, 但是它只是在形式上(写法上)和外部类有关系, 其实在逻辑上和外部类并没有直接的关系。而一般的内部类,不仅在形式上和外部类有关系(写在外部类的里面), 在逻辑上也和外部类有联系。

参考博客地址:https://www.cnblogs.com/hasse/p/5020519.html

反射机制

反射机制是Java语言提供的一种基础能力,赋予程序在运行时自省的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。反射可以绕过API访问控制。

动态代理

JDK Proxy和cglib【调用目标不便实现额外接口】 JDK动态代理:代理类和目标类实现了共同的接口,用到InvocationHandler接口。 CGLIB动态代理:代理类是目标类的子类,用到MethodInterceptor接口。

AOP

AOP通过动态代理机制可以大幅度提高代码的抽象程度和复用度。

对比Vector、ArrayList、LinkedList有何区别?

这三者都是实现集合框架中的List,也就是所谓的有序集合。Vector是Java早起提供的线程安全的动态数组;ArrayList是应用更加广泛的动态数组实现【Vector在扩容时会提高1倍,而ArrayList则是增加50%】;LinkedList顾明思义是Java提供的双向链表,它不需要像上面两种那样调整容量。它也不是线程安全的。 Vector和ArrayList作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。插入和删除往往性能会相对较差。 而LinkedList进行节点插入、删除却效率很高,但是随机访问性能则要比动态数组慢。

考察Java集合框架,我觉得有很多方面需要掌握

Java集合框架的设计结构,至少要有一个整体印象。 Java提供的主要容器(集合和Map)类型,了解或掌握对应的数据结构、算法,思考具体技术选择。

HashMap的实现原理

HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用LinkedList来解决碰撞问题,当发生碰撞了,对象将会储存在LinkedList的下一个节点中。 HashMap在每个LinkedList节点中储存键值对对象。  当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的LinkedList中。键对象的equals()方法用来找到键值对。 ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,只要多个修改操作发生在不同的段上,它们就可以并发进行。

冒泡排序

public static void bubbleSort(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   for (int end = arr.length - 1; end > 0; end--) {
      for (int i = 0; i < end; i++) {
         if (arr[i] > arr[i + 1]) {//如果前一个位置比后一个位置大,则交换位置
            swap(arr, i, i + 1);
         }
      }
   }
}

第k大数

public class Solution {
    public int kthLargestElement(int k, int[] nums){
        if(nums == null || nums.length == 0){
            return 0;
        }
        if(k <= 0){
            return 0;
        }
        return helper(nums,0,nums.length-1,nums.length-k);
    }
    public int helper(int[] nums, int l, int r, int k){
        if(l ==r){
            return nums[l];
        }
        int position  = partition(nums, l ,r);
        if(position == k){
            return nums[position];
        }else if(position < k){
            return helper(nums, position+1 ,r,k);
        }else {
            return helper(nums, l ,position-1,k);
        }
    }
    //快排模板
    public int partition(int[] nums,int l ,int r){
        //初始化左右指针
        int left = l,right =r;
        int pivot = nums[left];
        //进行partition
        while(left < right){
            while (left < right && nums[right] >= pivot){
                right--;
            }
            nums[left] = nums[right];
            while (left < right && nums[left] <= pivot){
                left++;
            }
            nums[right] = nums[left];
        }
        //返还pivot点到数据里面
        nums[left]= pivot;
        return left;
    }
}

快速排序

    //快速排序
    public void QuickSort(int a[], int left, int right) {
        if (left >= right) {
            return;
        }
            int center = partition(a, left, right);
            QuickSort(a, left, center - 1);//左半部分
            QuickSort(a, center + 1, right);//右半部分
    }
    //快排模板
    public int partition(int[] nums, int l, int r) {
        //初始化左右指针
        int left = l, right = r;
        int pivot = nums[left];
        //进行partition
        while (left < right) {
            while (left < right && nums[right] >= pivot) {
                right--;
            }
            nums[left] = nums[right];
            while (left < right && nums[left] <= pivot) {
                left++;
            }
            nums[right] = nums[left];
        }
        //返还pivot点到数据里面
        nums[left] = pivot;
        return left;
    }

前k大

    public List<Integer> topk(int[] nums, int k) {
        PriorityQueue<Integer> minheap = new PriorityQueue<>(k);

        for (int i : nums) {
            minheap.add(i);
            if (minheap.size() > k) {
                minheap.poll();
            }
        }
        List<Integer> res = new ArrayList<>(minheap);
        Collections.reverse(res);
        return res;
    }

二分法模板

public int binarySearch(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int start = 0, end = nums.length;
        while (start < end - 1) {
            int mid = start + (end - start) >> 1;
            if (nums[mid] <= target) {
                start = mid;
            } else {
                end = mid;
            }
        }
        if (nums[start] == target) {
            return start;
        }else {
            return -1;
        }
    }

单链表反转-----非递归版本

public Node reverse(Node head) {
        Node pre=null,cur=head,next=null;
        while (cur!=null){
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }

Redis Cluster

Redis-Cluster采用无中心结构。采用哈希槽的方式来分配16384个槽。 获取数据:如果存入一个值,按照redis cluster哈希槽的算法;CRC16('key')%16384=6782。那么就会把这个key的存储分配到B上,同样,当我连上(A,B,C)任何一个节点想获取'key'时,也会以这样的算法,内部跳转到B节点上获取数据。

线程池的原理

核心线程、工作队列、最大处线程、拒绝策略。

对比 Vector、ArrayList、LinkedList有何区别?

这三者都是实现集合框架中的List,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。 Vector是Java早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。

ArrayList是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与Vector近似,ArrayList也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector在扩容时会提高1倍,而ArrayList则是增加50%。

LinkedList 顾名思义是Java提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。

Vector和ArrayList作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差。

而LinkedList进行节点插入、删除却要高效很多,但是随机访问性能则要比动态数组慢。

所以,在应用开发中,如果事先可以估计到,应用操作是偏向于插入、删除,还是随机访问较多,就可以针对性的进行选择。

考察Java集合框架,我觉得有很多方面需要掌握: 1、Java集合框架的设计结构,至少要有一个整体印象; 2、Java提供的主要容器(集合和Map)类型,了解或掌握对应的数据结构、算法,思考具体技术选择。将问题扩展到性能、并发等领域; 3、集合框架的演进与发展。 数据结构和算法是基本功,往往也是必考的点,有些公司甚至以考察这些方面而非常知名,甚至是臭名昭著。你至少需要熟知: 1、内部排序,至少掌握基础算法比如归并排序、交换排序(冒泡、快排)、选择排序、插入排序等。 2、外部排序,掌握利用内存和外部存储处理超大数据集,至少要理解过程和思路。

考察算法不仅仅是如何简单实现,面试官往往会刨根问底,比如哪些排序是不稳定的呢(快排、堆排),或者稳定意味着什么;对不同数据集,各种排序的最好或最差情况;从某个角度如何进一步优化(比如空间占用,假设业务场景需要最小辅助空间,这个角度堆排序就比归并优异)等。

Java的集合框架,Collection接口是所有集合的根,然后扩展开提供了三大类集合,分别是: List,也就是我们前面介绍最多的有序集合,它提供了方便的访问、插入、删除等操作。 Set是不允许重复元素的,这是与List最明显的区别,也就是不存在两个对象 equals 返回 true。我们在日常开发中有很多需要保证元素唯一性的场合。 Queue/Deque,则是Java提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO)或者后入先出等特定行为。这里不包括BlockingQueue,因为通常是并发编程场合,所以被放置在并发包里。

每种集合的通用逻辑,都被抽象到相应的抽象类之中,比如AbstractList就集中了各种List操作的通用部分。这些集合不是完全孤立的,比如,LinkedList本身,既是List,也是Deque。

其实,TreeSet代码里实际默认是利用TreeMap实现的,Java类库创建了一个Dummy对象“PRESENT”作为value,然后所有插入的元素其实是以键的形式放入了TreeMap里面,同理,HashSet其实也是以HashMap为基础实现的,原来他们只是Map类的马甲!

TreeSet 是一个有序的Set集合,支持顺序访问,但是添加、删除、包含等操作要相对低效(log(n)时间)。 HashSet则是利用哈希算法,理想情况下,如果哈希散列正常,可以提供常数时间的添加、删除、包含等操作,但是它不保证有序。 LinkedHashSet,内部构建了一个记录插入顺序的双向链表,因此提供了按照插入顺序遍历的能力,与此同时,也保证了常数时间的添加、删除、包含等操作,这些操作性能略低于HashSet,因为需要维护链表的开销。

在遍历元素时,HashSet性能受自身容量影响,所以初始化时,除非有必要,不然不要将其背后的HashMap容量设置过大。而对于LinkedHashSet,由于其内部链表提供的方便,遍历性能只和元素多少有关系。

线程安全集合:

List list = Collections.synchronizedList(new ArrayList()); 它的实现,基本就是将每个基本方法,比如get、set、add之类,都通过synchronized添加基本的同步支持,非常简单粗暴,但也非常实用。注意这些方法创建的线程安全集合,都符合迭代fail-fast行为,当发生意外的并发修改时,尽早抛出 ConcurrentModificationException异常,以避免不可预计的行为。

理解Java提供的默认排序算法,具体是什么排序方式以及设计思路等。

这个问题本身就是有点陷阱的意味,因为需要区分是Arrays.sort()还是Collections.sort()【底层是调用Arrays.sort()】;什么数据类型;多大的数据集(太小的数据集,是没有必要采用复杂排序的,Java会直接进行二分插入排序)等。 1、对于原始数据类型,目前使用的是所谓双轴快速排序,是一种改进的快速排序算法。 2、而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并和二分插入排序(binarySort)结合的优化排序算法。TimSort并不是Java的独创,简单说它的思路是查找数据集中已经排好序的分区,然后合并这些分区来达到排序的目的。 3、Java 8 引入了并行排序算法(直接使用parallelSort方法),这是为了充分利用现代多核处理器的计算能力,底层实现基于fork-join框架,当处理的数据集比较小的时候,差距不明显,甚至还表现差一点;但是,当数据集增长到数万或百万以上时,提高就非常大了,具体还是取决于处理器和系统环境。

在Java 8 之中,Java平台支持了 Lambda 和 Stream,相应的 Java集合框架也进行了大范围的增强,以支持类似为集合创建相应 stream 或者 parallelStream的方式实现,我们可以非常方便的实现函数式代码。

阅读Java源代码,你会发现,这些API的设计和实现比较独特,它们并不是实现在抽象类里面,而是以默认方法的形式实现在Collection这样的接口里!这是 Java 8在语言层面的新特性,允许接口实现默认方法,理论上来说,我们原来实现在类似 Collections 这种工具类中的方法,大多可以转换到相应的接口上。

在Java 9 中,Java标准类库提供了一系列的静态工厂方法,比如,List.of()、Set.of(),大大简化了构建小的容器实例的代码量。根据业界实践经验,我们发现相当一部分集合实例都是容器非常有限的,而且在生命周期中并不会进行修改。但是,在原有的Java类库中,我们可能不得不写成:

ArrayList<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");

而利用新的容器静态工厂方法,一句代码就够了,并且保证了不可变性。

List<String> simpleList = List.of("Hello","World");

更进一步,通过各种of静态工厂方法创建的实例,还应用了一些我们所谓的最佳实践,比如,它是不可变的,符合我们队线程安全的需求;它因为不需要考虑扩容,所以空间上更加紧凑等。

如果我们去看of方法的源码,你还会发现一个特别有意思的地方:我们知道Java已经支持所谓的可变参数(varargs),但是官方类库还是提供了一系列特定参数长度的方法,看起来似乎非常不优雅,为什么呢?这其实是为了最优的性能,JVM在处理变长参数的时候会有明显的额外开销,如果你需要实现性能敏感的API,也可以进行参考。

HashMap 在并发环境可能出现无限循环占用 CPU

在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap。

HashMap 源码分析

HashMap设计与实现是个非常高频的面试题! 1、HashMap内部实现基本点分析。 它可以看作是数组和链表结合组成的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组的寻址;哈希值相同的键值对,则以链表形式存储。如果链表大小超过阈值,则链表就会被改造为树形结构。 具体键值对在哈希表中的位置: 仔细观察哈希值的源头,我们会发现,它并不是key本身的hashCode,而是来自于HashMap内部的另外一个hash方法。注意,为什么这里需要将高位数据移位到低位进行异或运算呢?这是因为有些数据计算出的哈希值主要在高位,而HashMap里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。

2、容量(capacity)和负载系数(load factor)。

预先设置的容量需要满足,大于”预估元素数量/负载因子“,同时它是2的幂数。

容量*负载因子 > 元素数量 3、树化。 如果链表大小超过阈值,则链表就会被改造为树形结构。

ConcurrentHashMap如何实现高效地线程安全

基于分段锁实现的ConcurrentHashMap 等并发实现。

早期ConcurrentHashMap,其实现是基于: 分离锁,也就是将内部进行分段(Segment),里面则是HashEntry的数组,和HashMap类似,哈希相同的条目也是以链表形式存放。 HashEntry内部使用volatile的value字段来保证可见性,也利用了不可变对象的机制以改进利用Unsafe提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能。 对于put操作,首先是通过二次哈希避免哈希冲突,然后以Unsafe调用方式,直接获取相应的Segment,然后进行线程安全的put操作:

既然Java反射可以访问和修改私有成员变量,那封装成private还有意义么?

private 就是一个关键字,对变量、方法或者类作用域的一种限制,用于正向编码。
而反射就是一个逆向工具,便于我们对一个未知的类进行解剖,如果没有反射机制,也就没有Java各类的框架的遍地开花,比如各类数据库驱动类,Spring,Hibernate等等。

synchroized的底层原理,字节码层次

当声明 synchronized 代码块时,编译而成的字节码将包含 monitorenter 和 monitorexit 指令。 关于 monitorenter 和 monitorexit 的作用,我们可以抽象地理解为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

相关文章

  1. 基于-SLF4J-MDC-机制的日志链路追踪配置属性

    ums: # ================ 基于 SLF4J MDC 机制的日志链路追踪配置属性 ================ mdc: # 是否支持基于 SLF4J MDC

  2. ajax-跨域访问

    ajax 跨域访问 &lt;!DOCTYPE html&gt; &lt;html xmlns:th="http://www.w3.org/1999/xhtml"&gt; &lt;head&gt;

  3. 给第三方登录时用的数据库表-user_connection-与-auth_token-添加-redis-cache

    spring: # 设置缓存为 Redis cache: type: redis # redis redis: host: 192.168.88.88 port

  4. Java动态代理

    Jdk动态代理 通过InvocationHandler和Proxy针对实现了接口的类进行动态代理,即必须有相应的接口 应用 public class TestProxy { public

  5. Java读取classpath中的文件

    public void init() { try { //URL url = Thread.currentThread().getContextClassLo

随机推荐

  1. 基于-SLF4J-MDC-机制的日志链路追踪配置属性

    ums: # ================ 基于 SLF4J MDC 机制的日志链路追踪配置属性 ================ mdc: # 是否支持基于 SLF4J MDC

  2. ajax-跨域访问

    ajax 跨域访问 &lt;!DOCTYPE html&gt; &lt;html xmlns:th="http://www.w3.org/1999/xhtml"&gt; &lt;head&gt;

  3. 给第三方登录时用的数据库表-user_connection-与-auth_token-添加-redis-cache

    spring: # 设置缓存为 Redis cache: type: redis # redis redis: host: 192.168.88.88 port

  4. Java动态代理

    Jdk动态代理 通过InvocationHandler和Proxy针对实现了接口的类进行动态代理,即必须有相应的接口 应用 public class TestProxy { public

  5. Java读取classpath中的文件

    public void init() { try { //URL url = Thread.currentThread().getContextClassLo