java常见疑难问题

来源:互联网 发布:网络剧影视推荐狗 编辑:程序博客网 时间:2024/05/16 04:17

1、关于"=="与equals中的误区

经常听到不少人说,在基础面试中,会被问及“关于'=='与equals中的区别”

而不少人都是回答:"关于‘==’是只负责基本数据类型比较,和引用对象地址比较,而equals则是比较两个对象间的内容是否一样"。甚至否写面试题目的答案中也是以此作为答案。

这答案当然不是全部错误,但是有相当一部分是误区,

每个类都是使用Object作为超类的,所有对象(包括数组)也实现这个类方法。

对于Object类的equals方法如下:

Java代码  收藏代码
  1. public boolean equals(Object obj) { 
  2.     return (this == obj); 

即是说关于一个普通类,没有作出覆盖equals方法时,改类的比较"=="与equals操作的结果是相同的。

而为什么再字符串中比较,我们强调要使用equals来比较内容,使用“==”会比较是否为同一对象。

是因为在String中equals方法已经覆盖如下:

Java代码  收藏代码
  1.    public boolean equals(Object anObject) { 
  2. if (this == anObject) { 
  3.     return true
  4. if (anObject instanceof String) { 
  5.     String anotherString = (String)anObject; 
  6.     int n = count; 
  7.     if (n == anotherString.count) { 
  8.     char v1[] = value; 
  9.     char v2[] = anotherString.value; 
  10.     int i = offset; 
  11.     int j = anotherString.offset; 
  12.     while (n-- != 0) { 
  13.         if (v1[i++] != v2[j++]) 
  14.         return false
  15.     } 
  16.     return true
  17.     } 
  18. return false
  19.    } 

根据字符串中的每一个字符作出比较(字符比较相当于基础数据类型比较)

而对于hashCode()是否要覆盖,

主要是在集合类中使用,例如set中为了保持唯一性,判断一个对象是否相等的时候,除了通过equals的值外,还需要通过判断hashCode是否相等

即:

对象相等,必然hashCode都应该相等

hashCode相等,对象未必相等。


2、Integer与int比较

jdk1.5引入了自动装箱(autoboxing)与自动拆箱(unboxing),这方便了集合类以及一些方法的调用,同时也使初学者对其感到非常之困惑。在此,我们来揭开其神秘的面纱。
首先,需要厘清一些概念:
1、Integer是一个类,用Integer声明一个变量其是一个对象类型(或者说引用类型);int是基本类型,用int声明的变量是非对象类型,即不能在其上调用方法。
2、“==”作用于对象上的时候,其比较的是对象的引用本身的值(或者说对象的地址更容易理解),而作用于基本类型的时候比较的就是基本类型的值。

接下来看一段代码:

public class Test {
    publicstaticvoid main(String[] args) {
        Integer i1 =2;
        inti2 =2;
        System.out.println(i1 == i2);
    }
}

在这段代码中有两个令人困惑的问题,首先是将一个基本类型的值赋值给对象的引用,即Integer i1 =2;其次是拿一个对象类型和一个基本类型比较。按理说这两种做法肯定都是有问题的,在jdk1.4(若使用的jdk版本是1.5或之后的版本中,可以使用javac -source 1.4 Test.java来编译)上,确实如此,第一个问题在编译时会报“不兼容的类型”错误,第二个问题会报“运算符 == 不能应用于 java.lang.Integer,int”的错误。

但是jdk1.5引入的自动装箱和自动拆箱,那么,必然要将其中的一种类型转换成另一种类型,究竟是将Integer对象i1转换成int基本类型呢?还是将int基本类型的i2转换成Integer对象?通过javap -c Test反编译Test.class文件就知道答案了:

public static void main(java.lang.String[]);
  Code:
   0:   iconst_2
   1:   invokestatic    #2;//Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   4:   astore_1
   5:   iconst_2
   6:   istore_2
   7:   getstatic       #3;//Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #4;//Method java/lang/Integer.intValue:()I
   14:  iload_2
   15:  if_icmpne     22
   18:  iconst_1
   19:goto    23
   22:  iconst_0
   23:  invokevirtual   #5;//Method java/io/PrintStream.println:(Z)V
   26:return
}

其中,[0-4]是Integer i1 = 2的实现,我们发现,编译的字节码里调用了Integer.valueOf方法,因此Integer i1 = 2编译后就等同于Integer i1 = Integer.valueOf(2);[5,6]是int i2 = 2的实现;[7,23]是System.out.println(i1 == i2)的实现,也容易看到,里面调用了Integer.intValue()方法。因此,这个i1 == i2这两个不同类型的变量比较,在编译的时候,编译器是将其转换成相同的类型进行比较的,即将对象类型转换成基本类型,System.out.println(i1 == i2)就等同于System.out.println(i1.intValue() == i2),前面说了,“==”作用于基本类型的时候比较的就是基本类型的值,两个值都是2,所以结果是true。

另外一个令人困惑的例子就是:

public class Test {
    publicstaticvoid main(String[] args) {
        Integer i1 =127;
        Integer i2 =127;
        System.out.println(i1 == i2);
        Integer i3 =128;
        Integer i4 =128;
        System.out.println(i3 == i4);
    }
}

运行后发现,i1==i2的结果为true,i3==i4的结果为false?这令不知原因的人头疼不已。在前面一个例子里我们已经说过,诸如Integer i1 = 127,在编译后就等同于Integer i1 = Integer.valueOf(127),既然是调用一个方法来获得对象,那么就有必要对valueOf方法一探究竟了。我们看下源码:

public static Integer valueOf(inti) {
    finalintoffset = 128;
    if(i >= -128&& i <= 127) {// must cache
        returnIntegerCache.cache[i + offset];
    }
    returnnewInteger(i);
}

到此应该恍然大悟了,IntegerCache缓存了[-128,127]之间的Integer对象,如果valueOf的参数i处于这之间,就返回缓存的对象。否则就new一个新的Integer。前面已经说过,“==”作用于对象上的时候,其比较的是对象的地址,例子中的i1和i2都是从缓存中拿的,当然是同一个对象,i3和i4都是通过new Integer获得的,当然不是同一个对象了。

类似地,java.lang.Long,java.lang.Short分别缓存了[-128,127]之间的Long和Short对象,java.lang.Byte缓存了所有的对象,java.lang.Character缓存了[0,127]之间的Character对象。java缓存这些对象是为了性能优化,既然我们已经知道其缓存了这么些对象,在需要new Integer/Long/...的地方,可改用Integer/Long/Short...#valueOf方法。

3、用StringBuilder(StringBuffer)#append替代字符串”+”会带来性能提升吗

经常看到一些论坛在谈java代码优化的时候讲到要将字符串连接操作"+"换成StringBuilder(或StringBuffer,后面为简单起见,只说StringBuilder)的append操作以提升性能,那么字符串连接使用StringBuilder#append来替代"+"真的会带来性能提升吗?不忙回答,先看几个例子,代码如下:

public class StringConcat {
    publicstaticvoid main(String... args) {
        concat1();
        concat2();
        concat3();
    }
    publicstaticvoid concat1() {
        String s ="today is "+ "a good day";
        System.out.println(s);
    }
    publicstaticvoid concat2() {
        intcount = 2;
        String tmp =" on the desk";
        String s2 ="there are "+ count +" books "+ tmp;
        System.out.println(s2);
    }
    publicstaticvoid concat3() {
        String s3 ="";
        for(inti=0; i<100; i++) {
            s3 = s3 + i;
        }
        System.out.println(s3);
    }
}

接下来分别分析下这三个操作字符串的方法,通过javap命令反编译.class文件:javap -c StringConcat ,获得字节码指令如下(只摘取concat1,concat2,concat3三个方法的):

public static void concat1();
  Code:
   0:   ldc     #5;//String today is a good day
   2:   astore_0
   3:   getstatic       #6;//Field java/lang/System.out:Ljava/io/PrintStream;
   6:   aload_0
   7:   invokevirtual   #7;//Method java/io/PrintStream.println:(Ljava/lang/String;)V
   10:return
public static void concat2();
  Code:
   0:   iconst_2
   1:   istore_0
   2:   ldc     #8;//String  on the desk
   4:   astore_1
   5new     #9;//class java/lang/StringBuilder
   8:   dup
   9:   invokespecial   #10;//Method java/lang/StringBuilder."<init>":()V
   12:  ldc     #11;//String there are
   14:  invokevirtual   #12;//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   17:  iload_0
   18:  invokevirtual   #13;//Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   21:  ldc     #14;//String  books
   23:  invokevirtual   #12;//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   26:  aload_1
   27:  invokevirtual   #12;//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   30:  invokevirtual   #15;//Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   33:  astore_2
   34:  getstatic       #6;//Field java/lang/System.out:Ljava/io/PrintStream;
   37:  aload_2
   38:  invokevirtual   #7;//Method java/io/PrintStream.println:(Ljava/lang/String;)V
   41:return
public static void concat3();
  Code:
   0:   ldc     #16;//String
   2:   astore_0
   3:   iconst_0
   4:   istore_1
   5:   iload_1
   6:   bipush100
   8:   if_icmpge     36
   11:new     #9;//class java/lang/StringBuilder
   14:  dup
   15:  invokespecial   #10;//Method java/lang/StringBuilder."<init>":()V
   18:  aload_0
   19:  invokevirtual   #12;//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   22:  iload_1
   23:  invokevirtual   #13;//Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   26:  invokevirtual   #15;//Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   29:  astore_0
   30:  iinc  1, 1
   33:goto    5
   36:  getstatic       #6;//Field java/lang/System.out:Ljava/io/PrintStream;
   39:  aload_0
   40:  invokevirtual   #7;//Method java/io/PrintStream.println:(Ljava/lang/String;)V

来分析下三个方法的字节码含义

在concat1中,是两个字面值(字符串常量)的连接,从concat1的字节码的第0条(0: ldc #5; //String today is a good day)可以看到,该方法直接从常量池加载"String today is a good day",也就是说,String s = "today is " + "a good day";这条语句在编译后已经变成了一个字符串,等效于String s = "String today is a good day",运行期间根本无需做连接操作了,所以对于字符串字面值的连接,使用StringBuilder是没有任何意义的

在concat2中,是变量参与字符串的连接。从反编译的字节码中可以看出,编译期间已经转换成了StringBuilder的append操作,

String s2 = "there are " + count + " books " + tmp;

语句在编译之后已经等效于(即[5,30]之间的指令):

String s2 = new StringBuilder().append("there are ").append(count).append(" books").append(tmp).toString();

由此可见,在这样的字符串连接代码里显式使用StringBuilder#append并不会带来性能的提升,因为String的“+”操作符在编译的时候已经被转换成StringBuilder#append了

最后看concat3,for循环中使用字符串连接,最后在for循环外使用连接后的字符串。字节码中的[11,29]之间是循环体,很容易发现,循环体中做了new StringBuilder的操作,字节码代表的代码含义如下:

String s3 = "";
for(inti=0; i<100; i++) {
    s3 =newStringBuilder().append(s3).append(i).toString();
}

在这种情况下,编译器的优化并不如我们的意,我们想要的优化代码是这样的:

String s3 = "";
StringBuilder tmp =newStringBuilder();
tmp.append(s3);
for(inti=0; i<100; i++) {
    tmp.append(i);
}
s3 = tmp.toString();

这对于编译器来说有些复杂了,我们需要手工才能做到。

综上三个方法的分析发现,用StringBuilder(StringBuffer)#append替代字符串"+"是否会带来性能提升并不是一成不变的,在不同的条件下情况也不相同,字符串字面值的连接在编译期间已经连接好了,普通的字符串连接并不需要显式的使用StringBuilder#append来增加效率,编译器已经给我们做掉了,在这种意义下,个人觉得string的"+"可以认为是StringBuilder#append的一个语法糖;但是如果形如concat3那种循环中的字符串连接,我们就需要显式使用StringBuilder了。在jdk1.4的时候,还没有StringBuilder类,编译器生成的优化代码使用的是StringBuffer。

针对String连接操作编译器生成的StringBuidler#append肯定是单个线程在操作,因此不会有线程安全问题。


4、try和finally里的return

finally一定会执行吗?回答当然是否定的,假如在try里执行了System.exit(0)就不会再去执行finally了

又如下面的代码,会打印什么内容?

public class Test {
    publicstaticvoid main(String... args) {
        System.out.println(getValue1());
        System.out.println(getValue2());
    }
    publicstaticint getValue1() {
        inti1 =0;
        inti2 =1;
        try{
            returni1;
        }finally {
            returni2;
        }
    }
    publicstaticint getValue2() {
        inti = 1;
        try{
            returni;
        }finally {
            i++;
        }
    }
}

这个问题可以通过反编译查看字节码指令来解释,编译后再运行javap -c Test即可得到方法要执行的指令,接下来分别对两个方法做个分析

以下对字节码的解释中【】表示栈,左边表示栈顶

public static int getValue1();
  Code:
   0:   iconst_0 //将0入栈,栈内容【0】
   1:   istore_0 //将栈顶元素弹出,也就是0,存到局部变量区索引为0的变量中(也就是i1),栈内容【】,(0,1)这两个指令是由int i1 = 0生成的
   2:   iconst_1 //将1入栈,栈内容【1】
   3:   istore_1 //将栈顶元素弹出,也就是1,存到局部变量区索引为1的变量中(也就是i2),栈内容【】,(2,3)这两个指令是由int i2 = 1生成的
   4:   iload_0  //将局部变量区索引为0的变量(也就是i1)值入栈,栈内容【0】
   5:   istore_2 //将栈顶元素弹出,也就是0,存到局部变量区索引为2的变量中(代码中没有声明,这是javac生成的临时变量,再此记为tmp1),栈内容【】
   6:   iload_1  //将局部变量区索引为1的变量(也就是i2)值入栈,栈内容【1】
   7:   ireturn  //将栈顶元素弹出,也就是1,并返回
   8:   astore_3 //(8,9,10)属异常处理部分,这段代码不会出现异常,故执行不到下面的指令
   9:   iload_1
   10:  ireturn
  Exception table:
   from   to  target type
     4     6     8   any
     8     9     8   any

可见如果finally和try里都有执行了return,try里的return的值会被废弃。

public static int getValue2();
  Code:
   0:   iconst_1  //将1入栈,栈内容【1】
   1:   istore_0  //将栈顶元素弹出,也就是1,存到局部变量区索引为0的变量中(也就是i),栈内容【】,(0,1)这两个指令是由int i = 1生成的
   2:   iload_0   //将局部变量区索引为0的变量(也就是i)的值入栈,栈内容【1】
   3:   istore_1  //将栈顶元素保存到局部变量区索引为1的变量中(代码中未声明此变量,在此记为tmp1),栈内容【】
   4:   iinc    0, 1 //将局部变量区索引为0的变量加1,栈内容【】
   7:   iload_1   //将局部变量区索引为1的变量(即tmp1)的值入栈,栈内容【1】
   8:   ireturn   //弹出栈顶值并返回,即返回1
   9:   astore_2  //以下是发生异常时的处理代码,这段代码不会抛出异常,后面的指令就不会执行到了
   10:  iinc    0, 1
   13:  aload_2
   14:  athrow
  Exception table:
   from   to  target type
     2     4     9   any
     9    10     9   any

由此可见,在try里返回值会先存到一个临时变量中,finally里改变的是原始变量,改完之后再将临时变量的值返回,也就是说在finally里改变返回值变量并不影响返回值本身。

5、java中i=i++问题分析

int i = 0;
i = i++;

结果还是0
为什么?

程序的执行顺序是这样的:因为++在后面,所以先使用i,“使用”的含义就是i++这个表达式的值是0,但是并没有做赋值操作,它在整个语句的最后才做赋值,也就是说在做了++操作后再赋值的,所以最终结果还是0

让我们看的更清晰点:

int i = 0;//这个没什么说的
i = i++;//等效于下面的语句:

int temp = i;//这个temp就是i++这个表达式的值
i++; //i自增
i = temp;//最终,将表达式的值赋值给i

这是java里的实现,当然在其他的语言如c或是c++中可能并不是这么处理的,每种语言都有各自的理由去做相应的处理。

这警示我们:不要在单个的表达式中对相同的变量赋值超过一次

让我们从字节码层次看一看,源码如下:

public class Test {
    publicstaticvoid main(String... args) {
        inti =0;
        i = i++;
        System.out.println(i);
    }
}

使用javac编译后再使用javap -c Test反编译这个类查看它的字节码,如下(只摘取main方法):

public static void main(java.lang.String[]);

Code:

0: iconst_0

1: istore_1

2: iload_1

3: iinc 1, 1

6: istore_1

7: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;

10: iload_1

11: invokevirtual #3; //Method java/io/PrintStream.println:(I)V

14: return

这里,我从第0行开始分析(分析中【】表示栈,栈的底端在左边,顶端在右边):

0:将常数0压入栈,栈内容:【0】

1:将栈顶的元素弹出,也就是0,保存到局部变量区索引为为1(也就是变量i)的地方。栈内容:【】

2:将局部变量区索引为1(也就是变量i)的值压入栈,栈内容:【0】

3:将局部变量区索引为1(也就是常量i)的值加一,此时局部变量区索引为1的值(也就是i的值)是1。栈内容:【0】

6:将栈顶元素弹出,保存到局部变量区索引为1(也就是i)的地方,此时i又变成了0。栈内容:【】

7:获取常量池中索引为2所表示的类变量,也就是System.out。栈元素:【】

10:将局部变量区索引为1的值(也就是i)压入栈。栈元素:【0】

11:调用常量池索引为3的方法,也就是System.out.println

14:返回main方法


6、“String s = new String("xyz");创建了多少个String实例