code学习

顺丰科技 java面试

作者:田维常

本来约的三点的面试,但是面试官提前上线看到我在线就说提前开始吧。

先看问题

  1. 自我介绍
  2. 说一个你认为有挑战的项目
  3. 怎么学习Java的
  4. 说一下抽象类和接口
  5. 说一下HashMap和Hashtable
  6. HashMap添加一个元素的流程
  7. 什么是红黑树,特点是什么 ?
  8. B+树的特点,有几层,最大可以存放多少条数据
  9. MySQL的索引为什么使用B+树而不使用跳表?
  10. Redis为什么使用跳表而不使用B+树或二叉树呢?
  11. 创建索引需要注意些什么?
  12. 如果单表数据量过千万,怎么优化?
  13. 一个500w条数据的表 a,一个300w数据的表 b,通过外键 tid 关联,如何最快地查询出满足条件的第50000到第50200中的这200条数据记录?
  14. 说说JVM的内存模型
  15. 为什么需要Survivor区?
  16. 你有什么想问我的吗?

试题解析

自我介绍

菜鸟的回答:

面试官你好,我叫张三,河南人,毕业于XX大学,从XX年毕业后就一直从事java开发,差不多3年了吧。来贵公司面试,寻求一份java开发工作。

自我介绍要说几个点:你是谁,你的优点是什么?这么多年你干了啥?在学校获得过什么奖?对哪些技术有深入研究?是否有高并发系统的设计?是否参与过什么大型项目?

总之,把你有的家底都亮出来,让人家知道你哪方面相对比较强。

说一个你认为有挑战的项目

这个问题其实是因人而异的,对于刚入门的朋友,叫他搭建个项目就觉得很有挑战性。

而对于大牛来说,“挑战性”已经不在是技术。更多的是何如包装项目哄好老板,如何压榨自己的下属才是项目有挑战性的点。

而在面试环节,有“挑战性”是对于面试官而言的一个标准。如果这个项目业务这个技术点面试官没接触过,听起来很难,那这个就是一个“有挑战”的项目。

如果面试官对你所说的挑战项目很熟悉,此时可能对你来说是个机会也是个挑战,回答出面试官没遇到的问题,已经如何解决的,那面试官妥妥的佩服你,反之,面试官知道的这个项目的问题,问你你答不上来,那就回面试大打折扣了。

比如说:五六年前,你的技术栈中有dubbo、Spring Boot 那是很吃香的,但是现在已经是标配了。

但是有大数据、高并发、架构改造经验的开发者还是少,因为绝大部分公司都没法发展成为大公司。但。这个也是随着软件工程怎么发展都无法改变的事。

所以对于有挑战的项目具有以下几个特点:

1、大数据量

2、高并发

3、架构改造

只要你的项目能和这几个东西沾一点边,那你的项目level就高至少一级。

这里,我给你一个回答的模板:

1、我负责的是这个xxx业务项目,这个业务的是用来xxx的。

2、前期为了快速试错,快速响应市场,前期使用了简单的xxxx方案。

3、随着业务的发展,这个方案在xxx方面出现xxx的技术问题。

4、为了解决这些技术难点,最终用了xxx方案,然后介绍这些方案,同时这些方案是怎么解决这些技术问题的。

平时都是怎么学习Java的

就如实地说自己的学习历程,但要注意,学习要体现出自己是主动的,另外,标注自己有个好习惯:记笔记,好几下不如烂笔头。

推荐看官网,看书,看视频。

学习过程中,不断实践,不断反思,不断总结。

说一下抽象类和接口

  • 抽象类要被子类继承,接口要被子类实现。
  • 抽象类可以有构造方法,接口中不能有构造方法。
  • 抽象类中可以有普通成员变量,接口中没有普通成员变量,它的变量只能是公共的静态的常量
  • 一个类可以实现多个接口,但是只能继承一个父类,这个父类可以是抽象类。
  • 抽象类中可以作方法声明,也可以做方法实现。接口中可以做方法声明,也可以定义default方法。
  • 抽象级别(从高到低):接口>抽象类>实现类。
  • 抽象类主要是用来抽象类别,接口主要是用来抽象方法功能。
  • 抽象类的关键字是abstract,接口的关键字是interface

说一下HashMap和Hashtable

我们可以从五个方面来回答:

  1. 线程是否安全: HashMap 是非线程安全的, HashTable 是线程安全的。因为 HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap);
  2. 效率: 因为线程安全的问题, HashMap 要比 HashTable 效率高一点。另外, HashTable基本被淘汰,不要在代码中使用它;
  3. 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException 。
  4. 初始容量带下和每次扩充容量大小的不同 :
  5. ① 创建时如果不指定容量初始值, Hashtable默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
  6. ② 创建时如果给定了容量初始值,那么Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小( HashMap 中的tableSizeFor()方法保证)。
  7. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度度大于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

尽管是普通不能再普通的面试题了,可面试中,照样很大部分人同学回答的不好。回答中提到了2的n次幂,面试官很有可能会继续追问相关的问题,如果还不清楚的,建议对HashMap进行系统的学习。

我的博客上之前发过两篇文章:

HashMap的31连环炮,我倒在第5个上

三年必备, HashMap 源码

HashMap添加一个元素的流程

HashMap在put添加元素过程可以分为下面9个步骤:

  • 1.使用put()方法时,直接调putVal()方法
  • 2.在put的时候先判断数组是否为空,如果为空则进行resize操作
  • 3.以hash索引数组的长度-1与key的hash值进行与运算,得出在数组中的索引,如果索引指定的位置为空,则代表可以插入,直接插入一个新的node
  • 4.判断当前的key是否存在,如果存在则进行替换,如果替换成功则返回老的值
  • 5.如果key不存在,则判断当前节点是否为树类型,如果是树类型的话,则按照树的操作去追加新节点内容
  • 6.如果出现hash冲突的节点不是树类型,则说明当前发生的碰撞在链表里面,则这个时候就进入循环处理逻辑
  • 7.进入循环逻辑之后先判断被碰撞节点的下一个节点是否为空,如果为空就将新节点放入
  • 8.放入后判断当前链表是否超过最大允许链表长度8,如果超过则转为红黑树进行插入
  • 9.如果map的索引表为空或者当前索引表长度还小于64(最大转红黑树的索引数组表长度),那么进行resize操作就行了;否则,如果被碰撞节点不为空,那么就顺着被碰撞节点这条树往后新增该节点插入
可以看看我博客上的博文:三年必备, HashMap 源码

什么是红黑树,特点是什么?

红黑树(Red Black Tree)是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。

红黑树的特点有5个:

  • 结点是红色或黑色。
  • 根结点是黑色。
  • 所有叶子都是黑色(叶子是NIL结点)
  • 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
  • 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。
其实这个问题不难,难得是可能有的面试官会问红黑树的操作,左旋转右旋转...,我面试过几百人,能说出来寥寥无几。

B+树的特点,有几层,最大可以存放多少条数据

B+树的特点有两个:

  • 1.非叶子节点]仅具有索引作用,也就是说,非叶子节点只能存储Key,不能存储value
  • 2.树的所有叶节点构成一个有序链表,可以按照key排序的次序依次遍历全部数据。

B+树一般是1道3层。

InnoDB页的大小默认是16KB:

  • 假设一条记录大小为1KB,则一个数据页中可以存16条数据(忽略页中的其他数据结构)
  • 假设主键为int,又指针大小为6B,则一个索引页中可以存储16KB/(4B+6B)≈1638个索引

所以,两层的B+树可以存储:16*1638=26208条数据;三层的B+树可以存储:16*1638*1638=42928704条数据。

MySQL的索引为什么使用B+树而不使用跳表?

B+树是多叉树结构,每个结点都是一个16k的数据页,能存放较多索引信息,所以扇出很高。三层左右就可以存储2kw左右的数据。也就是说查询一次数据,如果这些数据页都在磁盘里,那么最多需要查询三次磁盘IO。

跳表是链表结构,一条数据一个结点,如果最底层要存放2kw数据,且每次查询都要能达到二分查找的效果,2kw大概在2的24次方左右,所以,跳表大概高度在24层左右。最坏情况下,这24层数据会分散在不同的数据页里,也即是查一次数据会经历24次磁盘IO。

因此存放同样量级的数据,B+树的高度比跳表的要少,如果放在MySQL数据库上来说,就是磁盘IO次数更少,因此B+树查询更快。

而针对写操作,B+树需要拆分合并索引数据页,跳表则独立插入,并根据随机函数确定层数,没有旋转和维持平衡的开销,因此跳表的写入性能会比B+树要好。

其实,MySQL的存储引擎是可以换的,以前mysql 5.5是myisam,后来才有的innodb,它们底层索引用的都是B+树。也就是说,你完全可以造一个索引为跳表的存储引擎装到MySQL里。事实上,facebook造了个rocksDB的存储引擎,里面就用了跳表。直接说结论,它的写入性能确实是比innodb要好,但读性能确实比innodb要差不少。感兴趣的话,可以在文章最后面的参考资料里看到他们的性能对比数据。

Redis为什么使用跳表而不使用B+树或二叉树呢?

因为B+树的原理是 叶子节点存储数据,非叶子节点存储索引,B+树的每个节点可以存储多个关键字,它将节点大小设置为磁盘页的大小,充分利用了磁盘预读的功能。每次读取磁盘页时就会读取一整个节点,每个叶子节点还有指向前后节点的指针,为的是最大限度的降低磁盘的IO。因为数据在内存中读取耗费的时间是从磁盘的IO读取的百万分之一,而Redis是 内存中操作数据,不涉及IO,因此使用了跳表;

创建索引需要注意些什么?

这道题,也可以用在问你会哪些SQL优化的时候。
  • 最适合索引的列是出现在 WHERE 子句中的列,或连接子句中的列,而不是出现在 SELECT 关键字后的列。
  • 索引列的基数越大,索引效果越好。
  • 根据情况创建复合索引,复合索引可以提高查询效率。
  • 避免创建过多的索引,索引会额外占用磁盘空间,降低写操作效率。
  • 主键尽可能选择较短的数据类型,可以有效减少索引的磁盘占用提高查询效率。
  • 对字符串进行索引,应该定制一个前缀长度,可以节省大量的索引空间。

如果单表数据量过千万,怎么优化?

1、数据库设计和表创建时,考虑性能问题,比如:单表不要有太多字段,建议在20以内、索引并不是越多越好,要根据查询有针对性的创建,考虑在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描、选择合适的数据类型、选择合适索引类型等。

2、SQL编写时需要注意,比如:列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大、可通过开启慢查询日志来找出较慢的SQL、避免select *,将需要查找的字段列出来等。

3,存储引擎选择,MyISAM适合SELECT密集型的表,而InnoDB适合INSERT和UPDATE密集型的表 。

4、分库分表,比如:分库把一个数据库分成多个,建议做个读写分离就行了,真正的做分库也会带来大量的开发成本,得不偿失!不推荐使用、分表就是把一张大表,按照如上过程都优化了,还是查询卡死,那就把这个表分成多张表,把一次查询分成多次查询,然后把结果组合返回给用户。分表分为垂直拆分和水平拆分,通常以某个字段做拆分项。比如以id字段拆分为100张表: 表名为 tableName_id%100。但:分表需要修改源程序代码,会给开发带来大量工作,极大的增加了开发成本,故:只适合在开发初期就考虑到了大量数据存在,做好了分表处理,不适合应用上线了再做修改,成本太高等。

5、硬件升级,这办法是最简单的,相对的成本也高,老板就不愿意了。

6、数据库升级,比如:把MySQL数据库换成大数据引擎处理数据、换成阿里云POLARDB,POLARDB 是阿里云自研的下一代关系型分布式云原生数据库,100%兼容MySQL,存储容量最高可达 100T,性能最高提升至 MySQL 的 6 倍。

一个500w条数据的表 a,一个300w数据的表 b,通过外键 tid 关联,如何最快的查询出满足条件的第50000到第50200中的这200条数据记录?

方法一:如果 a 表 tid 是自增长,并且是连续的,b表的id为索引。SQL语句如下。

select * from a,b where a.tid = b.id and a.tid>500000 limit 200;           

方法二:如果 a 表的 tid 不是连续的,那么就需要使用覆盖索引,tid 要么是主键,要么是辅助索引,b 表 id 也需要有索引。SQL语句如下。

select * from b, (select tid from a limit 50000,200) a where b.id = a.tid;           

说说JVM的内存模型

JVM内存结构有: 程序计数器 、堆内存 、 方法区 和 栈 (java虚拟机栈和本地方法栈)。

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分, Eden空间 、 From Survivor空间 、 To Survivor空间 ,默认情况下年轻代按照 8:1:1 的比例来分配;

方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。方法区可理解为一种规范,其实现比如永久代、元空间。

Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。

为什么需要Survivor区?

如果没有Survivor,Eden区每进行一次Minor GC ,并且没有年龄限制的话, 存活的对象就会被送到老年代。这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。

面试官可能会问:执行时间长有什么坏处?

频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。

假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长。

假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。

所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16 次Minor GC还能在新生代中存活的对象,才会被送到老年代。

你有什么想问我的吗?

这问题有的面试官是礼貌性的问,也不是很注意你回答什么,因为此时估计面试就是凉凉啦。

但是,有反问的机会,大部分还是觉得你不错,被录取的概率非常大,所以还是得慎重回答

不管ta是怎么样的心态,咱们表现好自己就行。

这个问题看上去可有可无,其实很关键,一般面试官不喜欢说“没问题”的人,因为其很注重员工的个性和创新能力。企业不喜欢求职者问个人福利之类的问题,如果有人这样问:贵公司对新入公司的员工有没有什么培训项目,我可以参加吗?或者说贵公司的晋升机制是什么样的?企业将很欢迎,因为体现出你对学习的热情和对公司的忠诚度以及你的上进心。

求 关注+点赞+转发,谢谢啦!