java常见疑难问题

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 {
    public static void main(String[] args) {
        Integer i1 = 2 ;
        int i2 = 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 {
    public static void 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( int i) {
    final int offset = 128 ;
    if (i >= - 128 && i <= 127 ) { // must cache
        return IntegerCache.cache[i + offset];
    }
    return new Integer(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 {
    public static void main(String... args) {
        concat1();
        concat2();
        concat3();
    }
    public static void concat1() {
        String s = "today is " + "a good day" ;
        System.out.println(s);
    }
    public static void concat2() {
        int count = 2 ;
        String tmp = " on the desk" ;
        String s2 = "there are " + count + " books " + tmp;
        System.out.println(s2);
    }
    public static void concat3() {
        String s3 = "" ;
        for ( int i= 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
   5 new     # 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 :   bipush 100
   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 ( int i= 0 ; i< 100 ; i++) {
    s3 = new StringBuilder().append(s3).append(i).toString();
}

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

String s3 = "" ;
StringBuilder tmp = new StringBuilder();
tmp.append(s3);
for ( int i= 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 {
    public static void main(String... args) {
        System.out.println(getValue1());
        System.out.println(getValue2());
    }
    public static int getValue1() {
        int i1 = 0 ;
        int i2 = 1 ;
        try {
            return i1;
        } finally {
            return i2;
        }
    }
    public static int getValue2() {
        int i = 1 ;
        try {
            return i;
        } 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 {
    public static void main(String... args) {
        int i = 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实例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值