荐书:15道题测试你的Java⽔平
Java基础的书籍推荐
先占坑,后续整理读书笔记。
话题1:开门见⼭–测试你的Java
1.float类型在Java中占⽤4字节,long类型在Java中占⽤8字节,为什么float类型的取值范围⽐long类型的取值范围还⼤?
2.使⽤“+”可以连接两个字符串(String对象),那么,是怎样进⾏连接的?
3.构造器是否创建了对象?该怎样来证明这⼀点?
4.如果没有在类中显⽰声明构造器,则编译器会⾃动⽣成⼀个⽆参的构造器,那么编译器为什么要⾃动⽣成这个⽆参的构造器呢?有什么作⽤?
5.i++与++i到底有什么不同?仅仅是先加和后加的区别吗?
6.移位运算:5<<35,会⾸先进⾏35%32的求余运算吗?如果是这样,那么 5<<-2的结果是多少呢?
7.如果重写了equals⽅法,为什么还要重写hashCode⽅法?如果没有重写hashCode⽅法,会有什么问题呢?
8.从JDK1.7起,switch语句可以⽀持String类型,那么在底层是如何实现的?
9.静态⽅法是否可以重写?⽅法重写与⽅法隐藏有什么不同?
10.为什么不能在静态⽅法中使⽤this?this指代的是当前对象,但是,这个所谓的“当前对象”到底在哪⾥?
11.在Java中,类型会在什么时间、什么条件下由JVM加载?加载后⼀定会初始化吗?
12.⽐起C/C++中的枚举,Java中的枚举有什么不同(优势)?枚举是怎样实现的?
13.为什么要为String对象建⽴常量池?String常量池有什么好处?
14.每个基本数据类型都对应⼀个包装类型,这些包装类型有什么⽤?
15.内部成员类是如何绑定外围类对象的?
以上问题,如果你能回答0~5个,那说明你是⼀个Java新⼿,处于学习阶段,对Java语⾔有初步的了
解;如果你能回答6 ~10个,那你属于Java中级⽔平,处于成长阶段,具有⼀定的Java功底;如果你能回答11 ~14个,你属于Java中⾼级⽔平,处于成熟阶段,对Java有着较为深⼊的理解。如果你能回答全部问题,并且理解透彻,那想必你⼀定已经从事Java研究多年,是⼀个经验丰富的“⾼⼿”。
阅读本书,令你温故知新,使你“百尺竿头,更进⼀步”
<Java深⼊理解:透析Java本质的36个问题>
参考答案啊:
1.float类型在Java中占⽤4字节,long类型在Java中占⽤8字节,为什么float类型的取值范围⽐long类型的取值范围还⼤?
见 第1章话题6 扑朔迷离——浮点类型的种种悬疑
浮点类型只是近似的存储, 使⽤⼆进制能准确表⽰的⼩数实际上寥寥⽆⼏,在0.0001~0.9999这9999个浮点值中,可以使⽤⼆进制准确表达的只有15个。
数量级差很⼤的浮点运算
⼆进制所能表⽰的两个相邻的浮点值间存在⼀定的间隙,浮点值越⼤,这个间隙也会越⼤。当浮点值⼤到⼀定程度时,如果对浮点值的改变很⼩(对300000000加1),就不⾜以使浮点值发⽣改变,这就好⽐⼤海蒸发了⼀滴⽔,但还是⼤海,⼏乎没有变化。
2.使⽤“+”可以连接两个字符串(String对象),那么,是怎样进⾏连接的?
见 第3章 String类话题17 来龙去脉 “+”是怎样连接字符串的?
编译器是如何实现的?Oracle JDK 1.7的实现:
我们的问题并不是输出的结果,⽽是运算符“+”的连接原理。当使⽤“+”对字符串进⾏连接时,会创建⼀个临时的StringBuilder对象,该对象调动append⽅法负责字符串的连接操作,然后再调⽤StringBuilder类的toString⽅法转换成String对象。
注: 当使⽤运算符“+”连接字符串时,如果两个操作数都是编译时常量,则在编译时期就会计算出该字符串的值,⽽不会在运⾏的时候创建StringBuilder对象。
“+”的性能:当在循环中反复执⾏String对象连接时,建议直接使⽤StringBuilder来替代“+”的连接,这样可以省去每次系统创建StringBuilder类的开销。
java重写和重载的区别
3.构造器是否创建了对象?该怎样来证明这⼀点?
见 第4章⽅法、构造器与变量话题27 发轫之始执⾏初始化的构造器
很多初学者认为,是构造器创建了对象,并且在对象创建之后,将对象的地址返回。
void f(){
Object o =new Object();
}
如果构造器(或⽅法)的参数列表不为空,则在调⽤之前⼀定要先计算实际参数的值,然后才能调⽤构造器,因为调⽤时需要将实际参数赋值给形式参数。假设如果构造器创建了对象,则⼀定会先调⽤构造器,然后才会⽣成构造器所创建的对象。只有调⽤了构造器,才会得知对象是否创建成功。实验证明假设失败,构造器是在对象创建之后才调⽤的,当创建对象失败时,还没有对构造器的实际参数表达式求值,也就没有调⽤构造器。
构造器只是初始化类的实例,并没有创建类的实例。
那么究竟是谁创建了对象呢?
答案是,new运算符。在程序运⾏时,是new运算符在堆上开辟⼀定的空间,然后执⾏对象的初始(其中包括构造器),当对象创建成功时,也是new运算符将对象的起始地址返回给应⽤程序(并⾮是构造器返回的)。实际上,构造器的作⽤是在对象创建的时候进⾏类中实例成员的初始化,⽽绝⾮是创建对象。构造器也没有返回值。
从另⼀个⾓度来证明构造器没有创建对象的事实:
使⽤javap对类进⾏反编译,javap -c 类的全路径名
void f();
Code:
0:new #2//class java/lang/Object
3: dup
4: invokespecial #1//Method java/lang/Object."<init>":()V
7: astore_1
8:return
其中 new #2 代表创建对象指令,如果创建成功,将指向新创建对象的引⽤压⼊栈。invokespecial #1代表调⽤实例⽅法指令,根据索引#1所指向的常量池地址(该地址含有需要调⽤的实例⽅法信息)调⽤某实例⽅法或初始化⽅法(构造器)。这表明是,先创建,后调⽤构造⽅法进⾏初始化。
4.如果没有在类中显⽰声明构造器,则编译器会⾃动⽣成⼀个⽆参的构造器,那么编译器为什么要⾃动⽣成这个⽆参的构造器呢?有什么作⽤?
见 第4章⽅法、构造器与变量话题27 发轫之始执⾏初始化的构造器
如果我们在类中没有显⽰地声明构造器,则编译器会⾃动⽣成⼀个默认的构造器,该构造器参数列表为空,访问权限与类的访问权限相同。
默认的构造器有什么⽤呢?
如果⼀个构造器内没有使⽤this来调⽤同类中其他的构造器,则构造器的第1条语句就会使⽤super来调⽤⽗类的构造器,即使我们没有显式地使⽤super调⽤,编译器也会为我们补上这条语句。构造器递归的调⽤⽗类构造器,直到Object类为⽌。
因为继承关系的存在,⼦类可能会继承⽗类的某些成员,也就⽗类的实例初始化应该在⼦类的实例初始化之前进⾏。⼦类在构造器中需要⾸先调⽤⽗类的构造器来初始化⽗类的实例变量,然后再初始化⾃⼰的实例变量。故,如果类中没有声明构造器,编译器就会⽣成⼀个默认的构造器,该构造器并不是什么也不做,⽽是会⾸先调⽤⽗类的构造器:
super();
也就是说,默认的构造器⾄少调⽤了⽗类的构造器,还包括实例变量声明初始化与实例初始化块。具体见 第5章类与接⼝话题35 按部就班加载、链接与初始化
5.i++与++i到底有什么不同?仅仅是先加和后加的区别吗?
见 第2章运算符与表达式话题9 莫衷⼀是 i++j该如何把计算?话题10 千差万别 ++i与i++仅是“先加”与“后加”的差别吗?
如果我告诉你,后置++其实与前置++⼀样,在参与运算之前都会将变量的值加1,你信吗?嗯,应该是不信,不过,这是真的…
后置++的⾃⽩:实际上,不管是前置++,还是后置++,都是先将变量的值加1,然后才继续计算的。⼆者之间的真正的区别是:前置++是将变量的值加1后,使⽤增值后的变量进⾏运算的,⽽后置++是⾸先将变量赋值给⼀个临时变量,接下来对变量的值加1,然后使⽤临时变量进⾏运算.
int i =2;
int j = i++*30;
int temp = i;//将i赋值给⼀个临时变量,temp值为2
i +=1;//将i加1,值为3
j = temp *30;//使⽤临时变量temp进⾏运算,结果j为60
从指令上说,后置++在执⾏增值指令iinc前,先将变量的值压⼊栈,执⾏增值指令后,使⽤的是之前压⼊栈的值。
如果⼀条语句只执⾏前置++或者后置++,
i++;
++i;
那么它们是否完全等价呢?⼆者还要什么区别吗?
6.移位运算:5<<35,会⾸先进⾏35%32的求余运算吗?如果是这样,那么 5<<-2的结果是多少呢?
见第2章运算符与表达式话题12 移位运算移位运算的真实剖析
Java中定义了3种移位运算符, 分别是左移运算符"<<"、右移运算符">>“和⽆符号右移运算符”>>>", 对于移位运算两边的操作数要求为整型,即byte、short、char、int和long类型,或者通过拆箱转换后为整型。当操作数的类型为byte、short或char类型时,会⾃动提升为int类型,运算的结果也为int类型。
5<<35 超过⾃⾝位数的移位
int类型占⽤4字节,32位,long类型占⽤8字节,64位;如果左侧操作数为int类型(包括提升后为int类型),会对右侧操作数进⾏除数为32的求余运算,如果左侧操作数为long类型,会对右侧操作数进⾏除数为64的求余运算。
5<<35
35%32//结果为3
<0101<<3
0000 (00101000)
5<<-2 当右侧操作数为负数
当左侧操作数为int类型时(包括提升后为int类型),右侧操作数只有低5位是有效的(低5位的范围为0~31),也就是说可以看做右侧操作数会先与掩码0x1f做与运算(&),然后左侧操作数再移动相应的位数。
类似地,当左侧操作数为long类型时,右侧操作数只有低6位是有效的,可以看作右侧操作数先与掩码0x3f做与运算,然后再移动相应的位数。
-2的补码是:
//原码
1000 (0010)
//符号位不变,除符号位外逐位取反,最后再加1
<1101+1
1111 (1110)
取其低5位,结果为:11110, 16+8+4+2=30
5<<-2 就相当于5<<30
7.如果重写了equals⽅法,为什么还要重写hashCode⽅法?如果没有重写hashCode⽅法,会有什么问题呢?
见 第3章 String类话题21 旧调重弹再论equals⽅法与“==”的区别
我们需要弄清楚equals⽅法到底⽐较的是什么。从根源来看,equals⽅法是在Object类中声明的,所有类(除Object类⾃⾝外)都是Object 的直接或间接⼦类,都继承了这个public的⽅法。⽽在Object类中,
public boolean equals(Object obj){
return(this== obj);
}
从⽽可知,在Object类中,equals⽅法与“==”运算符是完全等价的。
我们在重写equals⽅法的时候,需要遵循⾃反性、对称性、传递性、⼀致性、⾮空不等于null等特性。是否重写了equals⽅法,具备以上⼏点就万事OK呢?No. ⼀旦涉及映射相关的操作(Map接⼝),还是会存在问题。因为实现了Map接⼝的类在调⽤put⽅法加⼊键值对或调⽤get⽅法取回值对象时,都是根据健对象的哈希码来计算存储位置的。如果两个对象通过调⽤equals⽅法是相等的,那么这两个对象调⽤hashCode⽅法必须返回相同的整数。如果⼀个类重写了equals⽅法,但没有重写hashCode⽅法,会导致严重后果…
8.从JDK1.7起,switch语句可以⽀持String类型,那么在底层是如何实现的?
见第2章运算符与表达式话题16 择⽊⽽栖开关选择表达式switch的类型内幕
在JDK1.5之前,switch表达式只可以是byte、short、char、int类型。从JDK1.5起,可以⽀持包装类型 Byte、Short、Character、Integer,以及枚举类型。从JDK1.7开始,switch表达式可⽀持String类型。
当switch表达式是String类型的时候,会将switch语句拆分成两个switch语句来处理,第1个switch语句根据对象的哈希码来对⼀个临时变量赋值,第2个switch语句根据该变量的值来匹配case表达式的值。
9.静态⽅法是否可以重写?⽅法重写与⽅法隐藏有什么不同?
见 第4章⽅法、构造器与变量话题25 踵事增华⽅法重写的真正条件 & 话题26 ⼀叶障⽬⽅法与成员变量的隐藏
⽅法重写的条件:
如果⼦类Sub的某个⽅法mSub重写了⽗类Super的某个⽅法mSuper,则需要满⾜以下条件:
1)mSub与mSuper都是实例⽅法;
2)mSub的签名是mSuper的⼦签名;
3)mSub的返回类型是mSuper返回类型的可替换类型;
4)mSub的访问权限不能低于mSuper的访问权限;
5)mSub不能⽐mSuper抛出更多的受检异常;
6)⼦类mSub继承了⽗类的mSub⽅法。
⽅法重写不同于⽅法重载,⽅法重载是根据实参的静态类型来决定调⽤哪个⽅法,⽽重写是根据运⾏时引⽤所指向对象的实际类型来决定调⽤哪个⽅法。
静态⽅法不可以重写。 ⽅法重写要求⽗类与⼦类的⽅法都是实例⽅法,如果其中有⼀个⽅法是静态⽅法,则会产⽣编译错误,如果两个⽅法都是静态⽅法,没有编译错误,但这种情况是⽅法隐藏,不是⽅法重写。
⽅法的隐藏与⽅法的重写形式上很相似,不同的是⽅法重写要求⽗类与⼦类的⽅法是实例⽅法,⽽⽅法隐藏要求⽗类与⼦类的⽅法是静态⽅法。对于成员变量⽽⾔,不管是实例变量还是静态变量,都不能重写,只能隐藏。
⽅法隐藏的条件:
如果⼦类Sub的某个⽅法mSub隐藏了⽗类Super的某个⽅法mSuper,则需要满⾜以下条件:
1)mSub与mSuper都是静态⽅法;
2)mSub的签名是mSuper的⼦签名;
3)mSub的返回类型是mSuper返回类型的可替换类型;
4)mSub的访问权限不能低于mSuper的访问权限;
5)mSub不能⽐mSuper抛出更多的受检异常;
6)⼦类mSub继承了⽗类的mSub⽅法。
重写与隐藏的本质区别:
在多态的表现上,重写与隐藏有着很⼤的区别。重写是动态绑定的,根据运⾏时引⽤所指向对象的实际类型来决定调⽤相关类的⽅法/成员。⽽隐藏是静态绑定的,根据编译时引⽤的静态类型来决定调⽤相关类的⽅法/成员。
换句话说,如果⼦类重写了⽗类的⽅法,当⽗类的引⽤指向⼦类对象时,通过⽗类的引⽤调⽤的是⼦
类的⽅法。如果⼦类隐藏了⽗类的⽅法(成员变量),通过⽗类的引⽤调⽤的仍然是⽗类的⽅法(成员变量)。
10.为什么不能在静态⽅法中使⽤this?this指代的是当前对象,但是,这个所谓的“当前对象”到底在哪⾥?
见 第4章⽅法、构造器与变量话题27 发轫之始执⾏初始化的构造器
this在哪⾥?
我们通常在构造器中使⽤this来访问由局部变量所遮蔽的成员变量,this(当前对象)是从哪⾥来的呢?⼀个类可以创建⽆数个对象,每个对象都会在堆上分配独⽴的空间,每个对象也都有⾃⼰的实例成员变量。当我们执⾏⽅法到this时,系统怎么知道我们要访问的是哪个对象的x变量呢?
在构造器或实例⽅法中,都包含⼀个隐藏的参数,这个参数就是类的对象,当调⽤构造器或者实例⽅法的时候,就会将这个参数作为第1个参数传递过去。
public static void main(String[] args){
T t =new T(28);
t.f();
}
//假设引⽤r指向new刚刚创建的对象,以上代码可以转化为:
public static void main(String[] args){
T t =new T(r,28);
t.f(t);
}
⾸先new运算符创建对象,然后会将指向新创建对象的引⽤®作为第1个参数隐式传递到构造器中。当调⽤实例⽅法时,也会将调⽤⽅法的对象引⽤(t)作为第1个参数隐式传递给实例⽅法。这就是this的由来了。
这⾥r指代的是new创建对象后,调⽤构造器之前,使⽤引⽤r指向的尚未初始化的对象。
⽽t此时还尚未初始化,只有当对象完全创建后,才会将引⽤返回,并赋值给t。
对于静态⽅法,在⽅法调⽤的时候是没有隐式参数(当前对象this)传递的,因为静态⽅法与对象⽆关,是与类相关联的,所以在静态⽅法中没有this。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。