【干货点】 该篇文章是好好面试系列的第一篇。我去年以面试官的身份面了多个候选人,同时也埋了多个候选人,今天主要是想给大家分享分享我在当面试官时在Java基础这块挖的坑,希望大家新的一年都可以避开这些坑,别再被埋了。
看看我在基础数据类型方面埋了什么坑
说说看,Java有多少种基本的数据类型?多大?
Java有8中基本的数据类型,分别是
- byte,占据1个字节8位
- char,占据2个字节16位
- short,占据2个字节16位
- int,占据4个字节32位
- float,占据4个字节32位
- long,占据8个字节64位
- double,占据8个字节64位
- boolean,占据一个字节8位
错,将boolean默认为一个字节基本是所有初学者的通。
注意:boolean的大小是未知的,虽然我们看boolean只有:true、false两种情况,可以使用 1 bit 来存储,但是实际上没有明确规定是1bit,因为因为对虚拟机来说根本就不存在 boolean 这个类型。在《Java虚拟机规范》中给出了两种定义,分别是4个字节和boolean数组时1个字节的定义,但是具体还要看虚拟机实现是否按照规范来,1个字节、4个字节都是有可能的。这其实是运算效率和存储空间之间的博弈,两者都非常的重要。
那Integer这些算什么呢?
这些算是包装类型,每个基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。比如:
Integer number1 = 2; // 装箱 调用了 Integer.valueOf(2)
int number2 = number1; // 拆箱 调用了 number1.intValue()
记得挺牢的,那 new Integer(1024) 和Integer.valueOf(1024) 有没有什么区别呢?
首先,new Integer(1024) 每次都会新建一个对象,而Integer.valueOf(1024) 会使用缓冲池中的对象,多次调用会取得同一个对象的引用。
我举个例子:
image-20210109100030722错,这是我埋着的坑点,我曾经用这一招坑了多个候选人。
image-20210109174541713注意:Integer.valueOf(1024) 和Integer.valueOf(1024) 缺不等于true,而是false。Integer.valueOf从缓冲池取的数值是有大小限制的,并不是任何数
我们可以看看valueOf() 的源码,其实也比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓冲池的内容。
image-20210109100401281目前我的jdk版本是 8 ,在jdk8中Integer 缓冲池的大小默认为 -128~127。
image-20210109100503563做为一个面试官,我很喜欢挖别人回答问题时暴露的细节点。你刚刚说到了自动装箱和拆箱,说说看你的理解?
编译器会在自动装箱过程中调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象,因此对比的时候会返回true。
image-20210109101748474继续往深挖,看看候选人对知识点的掌握有多深。那说说看你知道的缓冲池有哪些?
目前基本类型对应的缓冲池如下:
- boolean 缓冲池,true and false
- byte缓冲池
- short 缓冲池
- int 缓冲池
- char 缓冲池
因此我们在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,那么就可以直接使用缓冲池中的对象。
继续挖,看看他有没有看过缓冲池的源码。你说的这些缓冲池的上限下限都是不变的吗?还是说可以设定的?
基本上都是不可变的,不过在 jdk 1.8 中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的。我们可以看源码
image-20210109102339939在启动 jvm 的时候,我们可以通过通过 -XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小,在JVM初始化的时候,这个设置会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。
总结:上面的坑分别有boolean的大小、缓冲池的大小、自动拆箱和装箱、缓冲池的大小是否可变,基本上这几个坑点可以坑倒百分之六十的候选人,其次是做为一个面试官,我很喜欢挖候选人回答问题的细节,毕竟深挖可以看得出你是不是真的有料!!!
看看我在String方面埋了什么坑
你刚刚说了基本类型了,说说看你对String的了解吧
String 被声明为 final,因此它不可被继承。在 Java 8 中,String 内部使用 char 数组存储数据,并且声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组,String 内部也没有改变 value 数组的方法,因此可以保证 String 不可变。
继续深挖 说说看不可变的好处?
这个问题的回答比较泛,可以说的点比较多,大致可以分为:
首先是不可变自然意味着安全,当String 作为参数引用的时候,不可变性可以保证参数不可变。
其次是可以缓存 hash 值,实际上,我们开发的时候经常会用来当做map的key,不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
最后自然是String Pool 的需要,如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用,而自然只有 String 是不可变的,才可能使用 String Pool。如果是可变的,那么 String Pool也就无法被设计出来了。
继续深挖 有没有用过StringBuffer 和 StringBuilder,说说看String, StringBuffer 以及StringBuilder三者的区别?
首先他们都是被final修饰的类,都是不可被继承,不过从可变性上来说,String 我们刚刚说到了,是不可变的,而StringBuffer 和 StringBuilder 可变的,这是内部结构导致的,StringBuffer 和StringBuilder 内部放数据的数组没有被final修饰。
其次从线程安全方面来说
String 不可变,是线程安全的
StringBuilder 不是线程安全的,因为内部并没有使用任何的安全处理
StringBuffer 是线程安全的,内部使用 synchronized 进行同步
继续挖细节点你刚刚有说到String Pool ,说说看你的理解
String Pool也就是我们经常说的字符串常量池,它保存着所有字符串字面量,而且是在编译时期就确定了。
String Pool是在编译时期就确定了,那么请问是否不可变的呢?
是的。
错,所有初学者都会犯的一个问题,那就是忽略了String.intern的存在,我经常用这个坑点来区分初学者和中级水平的候选人的区别!!!
image-20210109174601615我们可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。
我们可以看到
image-20210109111528457这是一个本地方法,看不到源码,不过我们可以看到注释
大致意思就是当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
用个demo来解释这个流程
image-20210109111744460我上面的s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 和 s2.intern() 方法取得同一个字符串引用。第一个intern() 首先把 "饭谈编程" 放到 String Pool 中,然后返回这个字符串引用,而第二个intern()则直接从String Pool 读取了,因此 s3 和 s4 引用的是同一个字符串。
继续挖坑,准备埋了候选人刚刚说到 new String("饭谈编程") != new String("饭谈编程") ,那么 "饭谈编程" 和 "饭谈编程"相等吗?说下流程?
是相等的,我们可以看到
image-20210109112317572流程是因为:采用这种字面量的形式创建字符串,JVM会自动地将字符串放入 String Pool 中,因此它们两个是相等的。
继续往细节挖,这是一个比较刁钻的问题 new String("饭谈编程") JVM做了啥?
首先使用这种方式一共会创建两个字符串对象,当然了,前提是 String Pool 中还没有 "饭谈编程" 这个字符串对象,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "饭谈编程" 字符串字面量。
然后在使用 new 的方式的时候,在堆中创建一个字符串对象,这一步我们可以结合String的构造函数来看看
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,JVM会从String Pool 中将这个字符串对象取出来,当做参数传进String的构造函数中,将 value 数组和hash值赋予这个新的对象。
总结:String我们在日常开发中经常用到,不过一个合格的候选人应该要吃透String、StringBuilder 和StringBuffer的区别,并且要对String Pool的原理了解的尽量多一些,不要被我上面挖的坑给埋了。
看看我在运算方面埋了什么坑
请问在Java中方法参数的传递方式是引用传递呢?还是值传递呢?
这个要分情况,如果参数是基本类型的话,就是值传递,如果是引用类型的话,则是引用传递。
错,这是很多初学者容易搞错的地方,也是我日常挖坑埋人的地方
Java 的参数全都是是以值传递的形式传入方法中,而不是引用传递。如果参数是基本类型,则传递的是基本类型的字面量值的拷贝。而如果参数是引用类型的话,传递的则值该参数所引用的对象在堆中地址值的拷贝。
请看题 float f = 2.2,这么写有没有问题?
看起来是没问题的,其实是有问题的,这个其实一般我不会用来面试,而是用来放在笔试题中。
2.2这个字面量属于 double 类型的,因此不能直接将 2.2 直接赋值给 float 变量,因为这是向下转型,记住Java 不能隐式执行向下转型,因为这会使得精度降低。
正常写法是
float f = 2.2f;
继续挖坑,那么float f = 2.2f; f += 2.2;可以吗
这同样是我会放进笔试题考研候选人基础的一道题,是可以的,因为使用 += 或者 ++ 运算符,JVM会执行隐式类型转换。
上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:
f = (float) (f + 2.2);
总结:在运算方面埋的坑比较基础,一般是放在面试题中,而且其实用idea开发的话实际上可以在开发期就会报错了,但是这并不意味着idea可以检测出来的东西,你就可以不懂,特别是要来我司面试,这意味着你的专业能力是否过关。
看看我在修饰符方面埋了什么坑
说说看对修饰符final的理解
首先是在变量上使用了final,意味着声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量,作用可以分为:
- 对于基本类型,final 使数值不变;
- 对于引用类型,final 使引用不变,也就不能引用其它对象。
如果是在方法上使用了final,则声明方法不能被子类重写。
如果是在类上使用了final,则声明方法不允许被继承。
image-20210109123855200开始挖坑了,等着你跳 挺好的,按照你的说法,final int b = 1; b之后是不可以改的;那如果是这样的例子,A对象的x可以改吗
是可以改的,这也是引用类型的那一种,fianl是作用在A对象的引用上,而不是作用在A对象的数据成员x上,因此是可以改的。
image-20210109124438515继续挖坑 刚刚你说到声明方法不能被子类重写,那么问题来了,为啥这样可以
一般候选人都会在这里支支吾吾的说不出个所以然来。
image-20210109174614608其实他回答的理论是对的,只是他没有实际上尝试过我这种写法。实际上在private 方法隐式地被指定为 final的时候,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法并不是重写了基类方法,而是在子类中定义了一个新的方法。
聊聊看你对修饰符static的了解
首先是用在变量上的话,这个变量我们一般称之为静态变量,也可以称之为类变,也就是说这个变量属于类的,类所有的实例都共享静态变量,一般我们是直接通过类名来访问它,需要注意的一点事,静态变量在内存中只存在一份。
而如果是用在方法上的话,就被称之为静态方法,这个静态方法在类加载的时候就存在了,它不依赖于任何实例,因此静态方法必须有实现,也就是说它不能是抽象方法。
开始挖坑 可以在静态方法内使用this或者super关键字吗
不可以的,只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,可以说static和this和super是互相矛盾的存在。
image-20210109125553717坑点来了 之前来了个实习生,写代码的时候就犯了这个错,你看看下面的执行结果是啥
正确答案是
image-20210109125704464这里记住一个点就可以了,静态语句块优先于普通语句块,而普通语句块优先于构造函数。
image-20210109130202218继续深坑 那么如果是有继承关系在的时候呢?比如这道题,说说他们的执行顺序
大部分初级的候选人都会在这道题被绊倒,正确答案应该是:
image-20210109130258183也就是说,存在继承的情况下,初始化顺序为:
- 父类(静态变量、静态语句块)
- 子类(静态变量、静态语句块)
- 父类(实例变量、普通语句块)
- 父类(构造函数)
- 子类(实例变量、普通语句块)
- 子类(构造函数)
总结:虽然看起来修饰符是一个比较小的东西,但是如果实际开发中采坑了,却会造成比较大的风险,比如执行顺序搞错了,而且也可以通过候选人对修饰符的了解情况,可以看出这个人实际的编程水平,这也是我们作为面试官想要迫切知道的地方。
最后
后续系列文章安排:
- 谈谈我在Object挖的坑
- 谈谈我在集合挖的坑
- 谈谈我在netty系列挖的坑
…
你好,我是Java面试官饭谈编程,我将会从面试官角度告诉你我面试候选人期间挖的坑。
好好面试系列将会分多篇文章进行,基本上看完该系列的文章,Java基础这块便可以遇神杀神了,毕竟来来去去就这些,后续精彩请等待!