16.什么是不可变类?
不可变类指的是无法修改对象的值,比如 String 就是典型的不可变类,当你创建一个 String 对象之后,这个对象就无法被修改。
因为无法被修改,所以像执行s += "a";
这样的方法,其实返回的是一个新建的 String 对象,老的 s 指向的对象不会发生变化,只是 s 的引用指向了新的对象而已。
所以才会有不要在字符串拼接频繁的场景不要使用 + 来拼接,因为这样会频繁的创建对象。
不可变类的好处就是安全,因为知晓这个对象不可能会被修改,因此可以放心大胆的用,在多线程环境下也是线程安全的。
如何实现一个不可变类?
这个问题我被面试官问过,其实就参考 String 的设计就行。
String 类用 final 修饰,表示无法被继承。
String 本质是一个 char 数组,然后用 final 修饰,不过 final 限制不了数组内部的数据,所以这还不够。
所以 value 是用 private 修饰的,并且没有暴露出 set 方法,这样外部其实就接触不到 value 所以无法修改。
当然还是有修改的需求,比如 replace 方法,所以这时候就需要返回一个新对象来作为结果。
总结一下就是私有化变量,然后不要暴露 set 方法,即使有修改的需求也是返回一个新对象。
17.Java 按值传递还是按引用传递?
Java 只有按值传递,不论是基本类型还是引用类型。
基本类型是值传递很好理解,引用类型有些同学可能有点理解不了,特别是初学者。
JVM 内存有划分为栈和堆,局部变量和方法参数是在栈上分配的,基本类型和引用类型都占 4 个字节,当然 long 和 double 占 8 个字节。
而对象所占的空间是在堆中开辟的,引用类型的变量存储对象在堆中地址来访问对象,所以传递的时候可以理解为把变量存储的地址给传递过去,因此引用类型也是值传递。
18.泛型有什么用?泛型擦除是什么?
泛型可以把类型当作参数一样传递,使得像一些集合类可以明确存储的对象类型,不用显示地强制转化(在没泛型之前只能是Object,然后强转)。
并且在编译期能识别类型,类型错误则会提醒,增加程序的健壮性和可读性。
泛型擦除指的指参数类型其实在编译之后就被抹去了,也就是生成的 class 文件是没有泛型信息的,所以称之为擦除。
不过这个擦除有个细节,我们来看下代码就很清晰了,代码如下:
可以看到 yess 是有类型信息的,所以在代码里写死的泛型类型是不会被擦除的!
这也解释了为什么根据反射是可以拿到泛型信息的,因为这种写死的就没有被擦除!
至于泛型擦除是为了向后兼容,因为在 JDK 5 之前是没有泛型的,所以要保证 JDK 5 之前编译的代码可以在之后的版本上跑,而类型擦除就是能达到这一目标的一个实现手段。
其实 Java 也可以搞别的手段来实现泛型兼容,只是擦除比较容易实现。
19.说说强、软、弱、虚引用?
Java 根据其生命周期的长短将引用类型又分为强引用、软引用、弱引用、幻象引用。
- 强引用:就是我们平时 new 一个对象的引用。当 JVM 的内存空间不足时,宁愿抛出 OutOfMemoryError 使得程序异常终止,也不愿意回收具有强引用的存活着的对象。
- 软引用:生命周期比强引用短,当 JVM 认为内存空间不足时,会试图回收软引用指向的对象,也就是说在 JVM 抛出 OutOfMemoryError 之前,会去清理软引用对象,适合用在内存敏感的场景。
- 弱引用:比软引用还短,在 GC 的时候,不管内存空间足不足都会回收这个对象,ThreadLocal中的 key 就用到了弱引用,适合用在内存敏感的场景。 -虚引用:也称幻象引用,之所以这样叫是因为虚引用的 get 永远都是 null ,称为get 了个寂寞,所以叫虚。
虚引用的唯一作用就是配合引用队列来监控引用的对象是否被加入到引用队列中,也就是可以准确的让我们知晓对象何时被回收。
还有一点有关虚引用的需要提一下,之前看文章都说虚引用对 gc 回收不会有任何的影响,但是看 1.8 doc 上面说
简单翻译下就是:与软引用和弱引用不同,虚引用在排队时不会被垃圾回收器自动清除。通过虚引用可访问的对象将保持这种状态,直到所有这些引用被清除或者它们本身变得不可访问。
简单的说就是被虚引用引用的对象不能被 gc,然而在 JDK9 又有个变更记录:
链接:bugs.openjdk.java.net/browse/JDK-…
按照这上面说的 JDK9 之前虚引用的对象是在虚引用自身被销毁之前是无法被 gc 的,而 JDK9 之后改了。
我没下 JDK9 ,不过我有 JDK11 ,所以看了下 11 doc 的确实改了。
看起来是把那段删了。所以 JDK9 之前虚引用对引用对象的GC是有影响的,9及之后的版本没影响。
20.Integer 缓存池知道吗?
因为根据实践发现大部分的数据操作都集中在值比较小的范围,因此 Integer 搞了个缓存池,默认范围是 -128 到 127,可以根据通过设置JVM-XX:AutoBoxCacheMax=<size>
来修改缓存的最大值,最小值改不了。
实现的原理是int 在自动装箱的时候会调用Integer.valueOf,进而用到了 IntegerCache。
没什么花头,就是判断下值是否在范围之内,如果是的话去 IntegerCache 中取。
IntegerCache 在静态块中会初始化好缓存值。
所以这里还有个面试题,就是啥 Integer 127 之内的相等,而超过 127 的就不等了,因为 127 之内的就是同一个对象,所以当然相等。
不仅 Integer 有,Long 也是有的,不过范围是写死的 -128 到 127。
对了 Float 和 Double 是没有滴,毕竟是小数,能存的数太多了。
21.Exception 和 Error 的区别知道吗?
Exception 是程序正常运行过程中可以预料到的意外情况,应该被开发者捕获并且进行相应的处理。
Error 是指在正常情况下不太可能出现的情况,绝大部分的 Error 都会导致程序处于不正常、不可恢复的状态,也就是挂了。
所以不便也不需被开发者捕获,因为这个情况下你捕获了也无济于事。
Exception和Error都是继承了Throwable类,在Java代码中只有继承了Throwable类的实例才可以被throw或者被catch。
顺便我再提一提异常处理的注意点,之前写过文章总结过:
- 尽量不要捕获类似Exception这样通用的异常,而应该捕获特定的异常。
软件工程是一门协作的艺术,在日常的开发中我们有义务使自己的代码能更直观、清晰的表达出我们想要表达的信息。
但是如果你什么异常都用了Exception,那别的开发同事就不能一眼得知这段代码实际想要捕获的异常,并且这样的代码也会捕获到可能你希望它抛出而不希望捕获的异常。
- 不要"吞"了异常
如果我们捕获了异常,不把异常抛出,或者没有写到日志里,那会出现什么情况?线上除了 bug 莫名其妙的没有任何的信息,你都不知道哪里出错以及出错的原因。
这可能会让一个简单的bug变得难以诊断,而且有些同学比较喜欢用 catch 之后用e.printStackTrace(),在我们产品中通常不推荐用这种方法,一般情况下这样是没有问题的但是这个方法输出的是个标准错误流。
比如是在分布式系统中,发生异常但是找不到stacktrace。
所以最好是输入到日志里,我们产品可以自定义一定的格式,将详细的信息输入到日志系统中,适合清晰高效的排查错误。
- 不要延迟处理异常
比如你有个方法,参数是个 name,函数内部调了别的好几个方法,其实你的name传的是 null 值,但是你没有在进入这个方法或者这个方法一开始就处理这个情况,而是在你调了别的好几个方法然后爆出这个空指针。
这样的话明明你的出错堆栈信息只需要抛出一点点信息就能定位到这个错误所在的地方,进过了好多方法之后可能就是一坨堆栈信息。
- 只在需要try-catch的地方try-catch,try-catch的范围能小则小
只要必要的代码段使用try-catch,不要不分青红皂白try住一坨代码,因为try-catch中的代码会影响JVM对代码的优化,例如重排序。
- 不要通过异常来控制程序流程
一些可以用if/else的条件语句来判断例如null值等,就不要用异常,异常肯定是比一些条件语句低效的,有 CPU 分支预测的优化等。
而且每实例化一个Exception都会对栈进行快照,相对而言这是一个比较重的操作,如果数量过多开销就不能被忽略了。
- 不要在finally代码块中处理返回值或者直接return
在finally中return或者处理返回值会让发生很诡异的事情,比如覆盖了 try 中的return,或者屏蔽的异常。
22.深拷贝和浅拷贝?
深拷贝:完全拷贝一个对象,包括基本类型和引用类型,堆内的引用对象也会复制一份。
浅拷贝:仅拷贝基本类型和引用,堆内的引用对象和被拷贝的对象共享。
所以假如拷贝的对象成员间有一个 list,深拷贝之后堆内有 2 个 list,之间不会影响,而浅拷贝的话堆内还是只有一个 list。
比如现在有个 teacher 对象,然后成员里面有一个 student 列表。
因此深拷贝是安全的,浅拷贝的话如果有引用对象则原先和拷贝对象修改引用对象的值会相互影响。
23.面向对象编程和面向过程编程的区别?
面向对象编程(Object Oriented Programming,OOP)是一种编程范式或者说编程风格。
把类或对象作为基本单元来组织代码,并且运用提炼出的:封装、继承和多态来作为代码设计指导。
面向过程编程是以过程作为基本单元来组织代码的,过程其实就是动作,对应到代码中来就是函数,面向过程中函数和数据是分离的,数据其实就是成员变量。
而面向对象编程的类中数据和动作是在一起的,这也是两者的一个显著的区别。
如果对这两个概念还是比较模糊的话,可以看我写的这篇文章,4800多字来讲面向对象和面向过程,看完之后肯定懂!面向对象和面向过程解析
24.重载与重写的区别?
重载:指的是方法名相同,参数类型或者顺序或个数不同,这里要注意和返回值没有关系,方法的签名是名字和参数列表,不包括返回值。
重写:指的是子类重写父类的方法,方法名和参数列表都相同,也就是方法签名是一致的。重写的子类逻辑抛出的异常和父类一样或者是其父类异常的子类,并且方法的访问权限不得低于父类。
简单的理解为儿子不要超过爸爸,要尊老爱幼。
25.什么是内部类,有什么用?
内部类顾名思义就是定义在一个类的内部的类,按位置分:在成员变量的位置定义,则是成员内部类,在方法内定义,则是局部内部类。
如果用 static 修饰则为静态内部类,还有匿名内部类。
一般而言只会用成员内部类、静态内部类和匿名内部类。
成员内部类可以使用外部类的所有成员变量以及方法,包括 private 的。
静态内部类只能使用外部类的静态成员变量以及方法。
匿名类常用来作为回调,使用的时候再实现具体逻辑来执行回调。
实际上内部类是一个编译层面的概念,像一个语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和外部类没有任何区别,所以在 JVM 中是没有内部类的概念的。
一般情况下非静态内部类用在内部类和其他类无任何关联,专属于这个外部类使用,并且也便于调用外部类的成员变量和方法,比较方便。
静态外部类其实就等于一个顶级类,可以独立于外部类使用,所以更多的只是表明类结构和命名空间。
26.说说 Java 的集合类吧?
这种问题一般大致提一下,然后等着面试官深挖。
常用的集合有 List、Set、Map、Queue 等。
List 常见实现类有 ArrayList 和 LinkedList。
- ArrayList 基于动态数组实现,支持下标随机访问,对删除不友好。
- LinkedList 基于双向链表实现,不支持随机访问,只能顺序遍历,但是支持O(1)插入和删除元素。
Set 常见实现类有:HashSet、TreeSet、LinkedHashSet。
- HashSet 其实就是 HashMap 包了层马甲,支持 O(1)查询,无序。
- TreeSet 基于红黑树实现,支持范围查询,不过基于红黑树的查找时间复杂度是O(lgn),有序。
- LinkedHashSet,比 HashSet 多了个双向链表,通过链表保证有序。
Map 常见实现类有:HashMap、TreeMap、LinkedHashMap
- HashMap:基于哈希表实现,支持 O(1) 查询,无序。
- TreeMap:基于红黑树实现,O(lgn)查询,有序。
- LinkedHashMap:同样也是多了双向链表,支持有序,可以很好的支持 lru 的实现。
设置有序,并且重写LinkedHashMap中的 removeEldestEntry 方法,即可实现 lru。
这里有一点要提一下,如果你对某个东西比较熟悉就要在合适的地方抛出来。比如通过 LinkedHashMap 你还能延伸到 lru ,这表明你对 LinkedHashMap 有研究并且也知晓 lru,面试官自己可能都不清楚,会觉得你有点东西。
而且面试官基本会追问 lru 然后接着延伸,比如延伸到改进的 lru ,mysql 缓存中的 lru 等等,这就是通过你的引导把问题领域迁移到你自身熟悉的地方,这岂不美哉?如果你不熟悉,那少 bb。
Queue 常见的实现类有:LinkedList、PriorityQueue。
PriorityQueue:优先队列,是基于堆实现的,底层其实就是数组。
基本上回答不了这么全,稍微讲几个可能就被打断,然后深挖了,届时只能见招拆招。