为了账号安全,请及时绑定邮箱和手机立即绑定

去年面了多个候选人,看看我挖的坑还有他们应该要补的Java基础(一)

标签:
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,则声明方法不允许被继承。

开始挖坑了,等着你跳 挺好的,按照你的说法,final int b = 1; b之后是不可以改的;那如果是这样的例子,A对象的x可以改吗

image-20210109123855200

是可以改的,这也是引用类型的那一种,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基础这块便可以遇神杀神了,毕竟来来去去就这些,后续精彩请等待!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
107
获赞与收藏
436

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消