Java隐蔽问题
1、基本类型与引用类型的比较
1.1、如下四个变量,哪两个比较为 false
Integer i01 = 59;
int i02 = 59;
Integer i03 =Integer.valueOf(59);
Integer i04 = new Integer(59);
(1)Integer 为了节省空间和内存会在内存中缓存 -128~127 之间的数字;
(2)valueOf():调用该方法时,内部实现作了个判断,判断当前传入的值是否在-128~127之间且 IntergCache是否已存在该对象如果存在,则直接返回引用,如果不存在,则创建一个新对象
(3)基本类型存在内存的栈中,与引用类型比较时, 引用类型会自动装箱,比较数值而不比较内存地址;
Integer a=123;
Integer b=123;
System.out.println(a==b); // 输出 true
System.out.println(a.equals(b)); // 输出 true
a=1230;
b=1230;
System.out.println(a==b); // 输出 false
System.out.println(a.equals(b)); // 输出 true
1.2、自动装箱拆箱机制是编译特性还是虚拟机运行时特性?分别是怎么实现的?
自动装箱机制是编译时自动完成替换的。装箱阶段自动替换为了 valueOf 方法,拆箱阶段自动替换为了 xxxValue 方法;
对于 Integer 类型的 valueOf 方法参数如果是 -128~127 之间的值会直接返回内部缓存池中已经存在对象的引用,参数是其他范围值则返回新建对象;
而 Double 类型与 Integer 类型类似,一样会调用 Double 的 valueOf 方法,但是 Double 的区别在于不管传入的参数值是多少都会 new 一个对象来表达该数值(因为在指定范围内浮点型数据个数是不确定的,整型等个数是确定的,所以可以Cache)
注意:Integer、Short、Byte、Character、Long 的 valueOf 方法实现类似,而 Double 和 Float 比较特殊,每次返回新包装对象,对于两边都是包装类型的:== 比较的是引用,equals 比较的是值;对于两边有一边是表达式(包含算数运算): == 比较的是数值(自动触发拆箱过程),对于包装类型 equals 方法不会进行类型转换;
1.3.Integer i = 1; i += 1; 做了哪些操作
- Integer i = 1; 做了自动装箱:使用 valueOf() 方法将 int 装箱为 Integer 类型
- i += 1; 先将 Integer 类型的 i 自动拆箱成 int(使用 intValue() 方法将 Integer 拆箱为
int),完成加法运行之后的 i 再装箱成 Integer 类型
2、关于String +和StringBuffer的比较
在 String+写成一个表达式的时候(更准确的说,是写成一个赋值语句的时候)效率其实比 Stringbuffer更快
public class Main{
public static void main(String[] args){
String string = "a" + "b" + "c";
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a").append("b").append("c");
string = stringBuffer.toString();
}
}
2.1、String+的写法要比 Stringbuffer 快,是因为在编译这段程序的时候,编译器会进行常量优化。
它会将a、b、c直接合成一个常量abc保存在对应的 class 文件当中{},看如下反编译的代码:
public class Main{}
public static void main(String[] args){
String string = "abc";
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a").append("b").append("c");
string = stringBuffer.toString();
}
}
原因是因为 String+其实是由 Stringbuilder 完成的,而一般情况下 Stringbuilder 要快于 Stringbuffer,这是因为 Stringbuilder 线程不安全,少了很多线程锁的时间开销,因此这里依然是 string+的写法速度更快;
/* 1 */
String a = "a";
String b = "b";
String c = "c";
String string = a + b + c;
/* 2 */
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(a);
stringBuffer.append(b);
stringBuffer.append(c);
string = stringBuffer.toString();
下面我们举个例子:
public static void main(String[] args)
{
String a = "a";
String b = "b";
String c = "c";
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
String string = a + b + c;
if (string.equals("abc")) {}
}
System.out.println("string+ cost time:" + (System.currentTimeMillis() - start) + "ms");
start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(a);
stringBuffer.append(b);
stringBuffer.append(c);
String string = stringBuffer.toString();
if (string.equals("abc")) {}
}
System.out.println("stringbuffer cost time:" + (System.currentTimeMillis() - start) + "ms");
}
我们每个进行了1亿次,我们会看到string+竟然真的快于stringbuffer,是不是瞬间被毁了三观,我们来看下结果。
2.2、字符串拼接方式:+、concat() 以及 append() 方法,append()速度最快,concat()次之,+最慢
- 编译器对+进行了优化,它是使用 StringBuilder 的 append() 方法来进行处理的,编译器使用 append()方法追加后要同 toString() 转换成 String 字符串,变慢的关键原因就在于new StringBuilder()和toString(),这里可是创建了 10 W 个 StringBuilder 对象,而且每次还需要将其转换成 Stringconcat:
- concat() 的源码,它看上去就是一个数字拷贝形式,我们知道数组的处理速度是非常快的,但是由于该方法最后是这样的: return new String(0, count + otherLen, buf);这同样也创建了 10 W个字符串对象,这是它变慢的根本原因
- append() 方法拼接字符串:并没有产生新的字符串对象;
3、静态代码块、静态变量
其作用级别为类;构造代码块、构造函数、构造,其作用级别为对象
(1)静态代码块,它是随着类的加载而被执行,只要类被加载了就会执行,而且只会加载一次,主要用于给类进行初始化。
(2)构造代码块,每创建一个对象时就会执行一次,且优先于构造函数,主要用于初始化不同对象共性的初始化内容和初始化实例环境。
(3)构造函数,每创建一个对象时就会执行一次;同时构造函数是给特定对象进行初始化,而构造代码是给所有对象进行初始化,作用区域不同;
==> 通过上面的分析,他们三者的执行顺序应该为:静态代码块 > 构造代码块 > 构造函数。
3.1、Java 类初始化过程
- 首先,初始化父类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
- 然后,初始化子类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
- 其次,初始化父类的普通成员变量和代码块,在执行父类的构造方法;
- 最后,初始化子类的普通成员变量和代码块,在执行子类的构造方法;
3.2、不要在构造器里调用可能被重载的虚方法
父类构造器执行的时候,调用了子类的重载方法,然而子类的类字段还在刚初始化的阶段,刚完成内存布局:
public class Base {
private String baseName = "base";
public Base(){
callName();
}
public void callName(){
System. out. println(baseName);
}
static class Sub extends Base{
private String baseName = "sub";
@Override
public void callName(){
System. out. println (baseName) ;
}
}
public static void main(String[] args){
Base b = new Sub();
}
}
3.3、Java 中赋值顺序
(1)父类的静态变量赋值
(2)自身的静态变量赋值
(3)父类成员变量赋值
(4)父类块赋值
(5)父类构造函数赋值
(6)自身成员变量赋值
(7)自身块赋值
(8)自身构造函数赋值
3.4、Java 代码执行顺序
public class TestExecuteCode {
public static void main(String[] args) {
System.out.println(new B().getValue());
}
static class A {
protected int value;
public A(int v){
setValue(v);
}
public void setValue(int value) {
this.value = value;
}
public int getValue() {
try {
value++;
return value;
} finally {
this.setValue(value);
System.out.println(value);
}
}
}
static class B extends A {
public B(){
super(5);
setValue(getValue() - 3);
}
@Override
public void setValue(int value){
super.setValue(2 * value);
}
}
}
- 执行结果:22,34,17
(1)子类 B 中重写了父类 A 中的setValue方法:super(5) // 调用了父类构造器,其中构造函数里面的setValue(value),调用的是子类的setValue方法finally块中的:this.setValue(value) //调用的也是子类的setValue方法而子类setValue方法中的:super.setValue(2*value); //调用的是父类A的setValue方法。
(2)try…catch…finally块中有return返回值的情况:finally 块中虽然改变了value的值,但try块中返回的应该是 return 之前存储的值 - 父类执行时如果有子类的方法重写了父类的方法,调用的子类的重写方法
4、给出一个表达式计算其可以按多少进制计算
式子7*15=133成立,则用的是几进制?可以通过解方程来解决,上述式子可以转换为方程:
7 * (1 * x + 5) = 1 * x^2 + 3 * x + 3
x^2 -4x - 32 = 0
x = -4 或 x = 8
如果下列的公式成立:78+78=123,则采用的是___进制表示的:
7 * x + 8 + 7 * x + 8 = 1 * x^2 + 2 * x + 3
x^2 - 12 * x - 13 = 0
x = -1, x = 13
5、表达式的数据类型
5.1、基本类型中类型转换
- 所有的 byte,short,char 型的值将被提升为 int 型;
- 如果有一个操作数是 long 型,计算结果是 long 型;
- 如果有一个操作数是 float 型,计算结果是 float 型;
- 如果有一个操作数是 double 型,计算结果是 double 型;
- final 修饰的变量是常量,如果运算时直接是已常量值进行计算,没有final修饰的变量相加后会被自动提升为int型
byte b1=1,b2=2,b3,b6;
final byte b4=4,b5=6;
b6=b4+b5;// b4, b5是常量,则在计算时直接按原值计算,不提升为int型
b3=(b1+b2);// 编译错误
System.out.println(b3+b6);
记住一点:JDK中关于任何整型类型的运算,都是按照int来的
private static final long mil_seconds = 24 * 60 * 60 * 1000;
private static final long micro_seconds = 24 * 60 * 60 * 1000 * 1000;
public static void main(String[] args) {
System.out.println(micro_seconds / mil_seconds);
}
上面代码中 micro_seconds 在运算时,其已超过 int 类型的最大值,溢出了。另外,如果在基本类型与对应的包装类型进行比较或者运算的时候,都会将包装类型自动拆箱,例如下面的代码:
5.2、三目运算中类型转换问题
在使用三目运算符时,尽量保证两个返回值的类型一致,不然会触发类型转换,转换规则如下:
(1)如果返回值X和返回值Y是同种类型,那么返回类型毫无疑问就是这种类型;
(2)如果两个返回值X和Y的类型不同,那么返回值类型为他们两最接近的父类。举例:
// String 和 Boolean 都实现了 Serializable 接口
Serializable serializable = a == b ? "true" : Boolean.FALSE;
// 所有类都继承了 Object 类
Object o = a == b ? new ArrayList<>() : new TernaryOperatorDemo();
(3)对于基本数据类型,如果其中一个返回值X类型为byte、short或者char,另一个返回值Y类型为int:
- 若在编译期就能判断出Y的取值范围在X的取值范围之内,则返回类型为X的类型,反之则为Y的类型。
- 如果返回值X类型不为以上几种,则会触发隐藏类型转换;
(4)当基本数据类型和对象数据类型相遇时,三目运算默认返回结果为基本数据类型;
例子:
private static void test1(int a, int b) {
// 触发隐藏类型转换,int 类型 9 转为 9.0D
System.out.println(a == b ? 9.9 : 9);
// 编译期判断,98 在 char 之内,转为 b
System.out.println(a == b ? 'a' : 98);
// 编译期判断,超出char范围,统一转 int
System.out.println(a == b ? 'a' : Integer.MAX_VALUE);
// 编译期时无法判断 b 的取值,触发隐藏类型转换,统一转 int
System.out.println(a == b ? 'a' : b);
System.out.println(a != b ? 'a' : b);
Map<String, Long> map = new HashMap<>();
map.put("b", 1L);
// 基本数据类型和对象数据类型相遇时,默认转为基本数据类,
// map.get("a") 返回 null,转为基本数据类型时,报空指针异常
System.out.println(map == null ? -1L : map.get("a"));
}
6、按照目录结构打印当前目录及子目录
public class PrintDirectory {
public static void main(String[] args) {
File file = new File("E:\\下载");
PrintDirectory pd = new PrintDirectory();
pd.listDirectory(file,0);
}
//列出该目录的子目录
private void listDirectory(File dir,int level){
System.out.println(getSpace(level) + dir.getName());
level++;
File[] files = dir.listFiles();
for(int i=0;i<files.length;i++){
if(files[i].isDirectory()){
listDirectory(files[i],level);
}else{
System.out.println(getSpace(level)+files[i].getName());
}
}
}
//按照目录结构打印目录
private String getSpace(int level){
StringBuilder sb = new StringBuilder();
for(int i=0;i<level;i++){
sb.append("|--");
}
return sb.toString();
}
}
7、boolean占用字节数
- 在Java虚拟机中没有任何供 boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替。
- Java虚拟机直接支持 boolean类型的数组,虚拟机的navarra指令参见newarray小节可以创建这种数组。boolean类型数组的访问与修改共用byte类型数组的baload和bastore指令;
- 因为在虚拟机规范中说了,boolean值在编译之后都使用Java虚拟机中的int数据类型来代替,而int是4个字节,那么boolean值就是4个字节。
- boolean类型数组的访问与修改共用byte类型数组的baload和bastore指令,因为两者共用,只有两者字节一样才能通用呀,所以byte数组中一个byte是1个字节,那么boolean数组中boolean是1个字节。
总结:boolean在数组情况下为1个字节,单个boolean为4个字节
Java规范中,没有明确指出boolean的大小。在《Java虚拟机规范》给出了单个boolean占4个字节,和boolean数组1个字节的定义,具体 还要看虚拟机实现是否按照规范来,所以1个字节、4个字节都是有可能的。
class LotsOfBooleans{
boolean a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af;
boolean b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf;
boolean c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf;
boolean d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, da, db, dc, dd, de, df;
boolean e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, ea, eb, ec, ed, ee, ef;
}
class LotsOfInts{
int a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af;
int b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf;
int c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf;
int d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, da, db, dc, dd, de, df;
int e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, ea, eb, ec, ed, ee, ef;
}
public class Test{
private static final int SIZE = 100000;
public static void main(String[] args) throws Exception{
LotsOfBooleans[] first = new LotsOfBooleans[SIZE];
LotsOfInts[] second = new LotsOfInts[SIZE];
System.gc();
long startMem = getMemory();
for (int i=0; i < SIZE; i++) {
first[i] = new LotsOfBooleans();
}
System.gc();
long endMem = getMemory();
System.out.println ("Size for LotsOfBooleans: " + (endMem-startMem));
System.out.println ("Average size: " + ((endMem-startMem) / ((double)SIZE)));
System.gc();
startMem = getMemory();
for (int i=0; i < SIZE; i++) {
second[i] = new LotsOfInts();
}
System.gc();
endMem = getMemory();
System.out.println ("Size for LotsOfInts: " + (endMem-startMem));
System.out.println ("Average size: " + ((endMem-startMem) / ((double)SIZE)));
// Make sure nothing gets collected
long total = 0;
for (int i=0; i < SIZE; i++) {
total += (first[i].a0 ? 1 : 0) + second[i].a0;
}
System.out.println(total);
}
private static long getMemory(){
Runtime runtime = Runtime.getRuntime();
return runtime.totalMemory() - runtime.freeMemory();
}
}
另外,大部分指令都没有支持整数类型byte、char、short。编译器在编译期或运行期将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零位扩展为相应的int类型数据;