游戏开发

网易的Java服务端技术面总结

2021-07-18  本文已影响0人  higher2017

       说下这次面试的过程:猎头先联系我问我有没有去网易面试的意向,我本着想混混面试经验的目的就答应下来了。猎头联系我的第3天,网易就安排了第一次技术面。面试是远程进行的,耗时1个半小时。下面来分析下技术面一面的内容(部分问题忘记了,这里列出我有印象的):

面试官的问题:

  1. ArrayList的结构是怎么样的?如何保证在扩容数组时保证高效率?如果考虑扩容 add的时间复杂度是多少?
  2. HashMap的结构是怎么样的?除了数组加链表(红黑树)这种结构,还有没有其他结构的HashMap(这个问题我记得不是很清楚,有可能问的是还有没有其他结构的Map)?
  3. List<? super T>、List<? extend T>、Enum<E extend Enum<E>>三种泛型的区别。1、2里面可以加什么类型的对象,取出的对象类型是什么?
  4. Java有哪些原生线程池?构建这些线程池传的参数都有什么?这些参数作用是什么?
  5. 类是如何被加载的?ClassLoader是怎么工作的?一个class能否被两个不同的ClassLoader加载?
  6. 说一下你最近做过的一个大功能。

题目解说:

       网易的技术一面试题都并不难,考的比较基础而且实在。主要是围绕候选人基础知识的方向去考,没有太多很偏很难的问题。如果想要回答好这些题,并且给面试官留下好的印象。那么就不能在回答的时候仅仅就问题本身作答,要回答的有深度并且扩展更多自己有把握的知识点,比如:说出问题背后知识的原理和设计哲学;说出自己使用的经验和心得,我这里只对问题本身做一些知识背景介绍,各位可以思考一下当你面对这些问题时,什么样的答案才是让你自己满意的答案。

  1. ArrayList(面试老搭档了)

    • 底层是用数组来保存有序数据的,每一次add都先判断数组长度是否足够,如果数组长度不够就扩容,然后往当前位置的后一位插入数据。remove的过程并不会缩小数据组的长度,如果remove的位置后面还有数据,就把后面的数据复制前一位,然后把最后一位置空。
    • 先创建一个扩容后长度的数组,然后用System.arraycopy方法来对数据对象进行浅复制,最终得到扩容后的数组
    • 我们基本可以把每次扩容看作是数组长度翻一倍,这样的话add的时间复杂度就是log2
  2. HashMap(面试老搭档了)

    • 结构:数组加链表的形式,key的hash值决定这个键值对在数组中的位置,如果出现位置重复(碰撞),那么该位置就会以链表(或红黑树)的结构存多个值。
    • 还有没有其他结构的Map:树+链表的形式也可以。理论上只要能通过key快速定位键值对在这个结构中的位置就行,树结构就是在树中搜索key所在位置找到键值对。
  3. 泛型(泛型加上类的继承、接口实现,能有效扩大泛型的使用场景)

    • List<? super T>:能添加的元素是T或者以T为父类的对象(添加的任意元素能够直接转化成T的任何一个超类),取出来的对象为Object类型。含义:Lits泛型接受T,或者其父类之中包含T的对象(有多重继承的只要父类链条中包含T即可)。List<? super T>是被设计用来添加数据的泛型,并且只能添加T类型或其子类类型的元素。下面的代码使得这一点更容易理解。
        class Car {}
    
        class Bus extends Car {}
    
        void genSuperTest() {
           List<? super Car> carList = new ArrayList<>();
           List<? super Bus> busList = new ArrayList<>();
    
           fatherList.add(new Car()); // 编译通过
           fatherList.add(new Bus()); // 编译通过,Bus的直接父类是Car(Bus能转成Car),所以能够加进List<? super Car>里面
    
           sonList.add(new Car()); // 编译错误 - Car不能转成Bus对象
           sonList.add(new Bus()); // 编译通过
        }
    
        void test(List<? super Bus> list) {
           Object obj = list.get(0); // 编译通过,注意这里取出来的对象被抹除为Object类型了
        }
    
    • List<? extend T>:无法往这个list中add对象,这里为什么不能add对象也是一个考点 ———— List<? extend T>要满足队列中任意元素都是T的一个子类,也就是不能存在既有A又有B的情况(A和B都是T的子类)。并且取出的对象类型是T。List<? extends T>是被设计用来读取数据的泛型,并且读取类型为T。
       private static void genExtendTest() {
          List<? extends Car> fatherList = new ArrayList<>();
          List<? extends Bus> sonList = new ArrayList<>();
    
          fatherList.add(new Car()); // 编译错误,无法往List<? extend T>中add对象
          fatherList.add(new Bus()); // 编译错误,无法往List<? extend T>中add对象
    
          sonList.add(new Car()); // 编译错误,无法往List<? extend T>中add对象
          sonList.add(new Bus()); // 编译错误,无法往List<? extend T>中add对象
       }
    
       private static void test(List<? extends Bus> list) {
          Bus bus = list.get(0); // 编译通过,并且取出来的对象就是Bus
       }
    

    关于List<? super T>和List<? extend T>更多信息可以看:https://blog.csdn.net/qq_33591903/article/details/82746794

  4. Java原生线程池(下面这个四个是原生线程池):一开始我所理解的Java原生线程池就是java.util.concurrent.Executors中几个公开的静态方法生成的线程池。所以我就简单说了一下这个几个线程池的内容(简单介绍和用途):

    • FixedThreadPool:固定线程数量的线程池,线程池中线程数量不会超过用户指定的线程数量。任务队列最大长度:Integer.MAX_VALUE(注意避免OOM)。实现类是:ThreadPoolExecutor
    • SingleThreadExecutor:单线程的线程池,线程池中最多只会存在一个线程。任务队列最大长度:Integer.MAX_VALUE(注意避免OOM)。实现类是:ThreadPoolExecutor
    • CachedThreadPool:可缓存线程的线程池。如果线程池空闲,会按照一定策略回收空闲线程(空闲线程存活时间为60s)。当线程池中没有空闲线程时,它会创建一个新线程来执行新任务(不会进入任务队列等待执行)。实现类是:ThreadPoolExecutor
    • ScheduledThreadPoolExecutor:支持定时任务和周期任务的线程池。线程池中线程数量不会超过用户指定的线程数量。实现类是:ScheduledThreadPoolExecutor
      关于Java有哪些原生线程池的问题,我在面试期间的回答主要是上面这些。但是面试之后我回顾这个知识点的时候发现,我的回答可能并不是面试官想要的答案。上面这些线程池讲到底都是ThreadPoolExecutor,只不过是不同的策略。FixedThreadPool; SingleThreadExecutor; CachedThreadPool在线程创建和任务执行上机理都是一样的。
      如果从更深层次考虑,我认为回答:ForkJoinPool; ScheduledExecutorService; ThreadPoolExecutor会更合理。下面是对这三个线程池实现类的介绍:
    • ForkJoinPool:该线程池的实现机制就是将大任务拆成多个小任务(fork过程),然后再将多个小任务处理结果汇总得到最终的结果(join过程)。就是分治的思想,提供一个线程池来解决大规模的计算问题;
    • ScheduledExecutorService:这个线程池时在ThreadPoolExecutor的基础上,添加了定时任务和周期任务执行的功能;
    • ThreadPoolExecutor:常用线程池;
      下图是java原生线程池的接口、类实现的继承图:
      image.png

关于线程池再推荐一篇文章:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html 里面对线程池进行了系统全面的讲解。

  1. 类的加载(你写的代码到JVM执行的流程):
    • 类是如何被加载的:
      1. 编译:先是编译器将源码文件(一般是xxx.java文件)编译成.class文件,在编译器中完成;
      2. 加载:将.class文件加载到内存中,在JVM中完成;
      3. 验证:验证加载的.class文件是否符合虚拟机规范(这一步一般的编译器也会做,但是虚拟机要保证正确性),在JVM中完成;
      4. 准备:为该类分配内存(静态变量占的空间等等),在JVM中完成;
      5. 解析:解析类、方法、字段名为引用(调用方法的时候通过引用来调用),在JVM中完成;
      6. 初始化:静态变量赋值、static模块执行,在JVM中完成;
    • ClassLoader是怎么工作的:这里我通过源码来解释:
      protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
          Class<?> clazz = findLoadedClass(className); // 1. 判断自己这个ClassLoader是否已加载了这个class
          if (clazz == null) {
              ClassNotFoundException suppressed = null;
              try {
                  clazz = parent.loadClass(className, false); // 2. 委托自己的父加载器加载
              } catch (ClassNotFoundException e) {
                  suppressed = e;
              }
              if (clazz == null) {
                  try {
                      clazz = findClass(className); // 3. 自己的父加载器都没有加载到,就自己加载
                  } catch (ClassNotFoundException e) {
                      e.addSuppressed(suppressed);
                      throw e;
                  }
              }
          }
          return clazz;
      }
      
    • 一个class能否被两个不同的ClassLoader加载(这个知识点就涉及到我的知识盲区了):答案是可以。JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例(ClassLoader对象)加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。
      下面这段是验证代码(需要用到第三方包:com.itranswarp:compiler:1.0):
        public class ClassLoaderTest {
           static final String JAVA_SOURCE_CODE = "package com.test; public class Test {}";
    
           @Test
           public void test() throws Exception {
              JavaStringCompiler compiler = new JavaStringCompiler();
              // 将源码解析为二进制流,以供ClassLoader加载(同一份源码,转换出来的二进制流是一样的)
              Map<String, byte[]> results = compiler.compile("Test.java", JAVA_SOURCE_CODE);
              // 自定义的classLoader加载class
              Class<?> clazz = compiler.loadClass("com.test.Test", results);
    
              JavaStringCompiler compiler1 = new JavaStringCompiler();
              // clazz和clazz1是由不同的ClassLoader对象加载出来的
              Class<?> clazz1 = compiler1.loadClass("com.test.Test", results);
              
              assertNotEquals(clazz, clazz1);
              assertNotEquals(clazz.newInstance().getClass(), clazz1.newInstance().getClass());
           }
        }
    
  2. 根据自己实际情况说明。建议不要投机取巧,把自己明明没有参与的功能说成是自己完成的。

个人总结:

       这次面试还是比较顺利的,题目难度比较低不过面试官喜欢深挖,有几次甚至问到很偏的源码实现。因为疫情还没有彻底结束,所以采用的是远程的方式。

个人对面试的两点建议:

上一篇下一篇

猜你喜欢

热点阅读