在学习Cloneable接口的clone()方法前,有必要深入了解一下Java中的对象在内存中是如何创建的。

equals 和 ==

相信大家都会知道这两个的不同,== 是比较两个对象的引用是否指向同一个对象,说白了就是直接比较内存中的地址。而equals是比较两个对象的内容()。觉得这样说还是比较笼统。下面就来看看内存中的对象和基本数据类型是如何创建的把。

基本数据类型

Java中有八种基本数据类型。

  • byte
  • int
  • short
  • long
  • float
  • double
  • char
  • boolean

由于Java是完全面向对象的语言,因此Java为每一个基本数据类型都提供了一个包装类,并在JDK1.5之后,提供了自动装箱和拆箱功能。

这八种针对基本数据类型的包装类分别是

  • Byte (byte)
  • Integer (int)
  • Short (short)
  • Long (Long)
  • Float (float)
  • Double (double)
  • Character (char)
  • Boolean (boolean)

所谓的自动装箱:

  1. Integer i = 1000;

系统就会调用 Integer i = Integer.valueOf(i);自动装箱。

这里不得不再说说Java中的常量池。因为基本数据类型的使用量是很大的,Java常量池,在节省内存方面是一个很好的机制,相同的数据,在常量池中只要保留一份即可。Java的8种基本类型,除Float和Double以外, 其它六种都实现了常量池, 但是它们只在大于等于-128并且小于等于127时才使用常量池。

查看Integer中valueOf(int i)方法的源代码:

  1. public static Integer valueOf(int i) {
  2. return i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128];
  3. }
  4. private static final Integer[] SMALL_VALUES = new Integer[256];
  5. static {
  6. for (int i = -128; i < 128; i++) {
  7. SMALL_VALUES[i + 128] = new Integer(i);
  8. }
  9. }

发现Integer类中有一个 static final类型的Integer数组SMALL_VALUES,数组大小是256,再看下面的static代码块,这是一个静态代码块,在加载该类的时候就首先执行静态代码块。在这个静态代码块中,对这个数组进行初始化。当调用value(int i)方法时,首先判断这个i的大小,如果大于等于-128,小于等于127,那么直接放回数组中定义好的Integer对象,否则才为我们创建新对象。

我们用一个简单的例子来看看, 用“==”运算符比较对象的地址。

  1. Integer i1 = 100;
  2. Integer i2 = 100;
  3. System.out.println( i1 == i2); //true ,-128=< 100 <= 127 返回 同一个对象,地址一样
  4. Integer i3 = 1000;
  5. Integer i4 = 1000;
  6. System.out.println( i3 == i4); //false,创建新的对象,地址不一样
  7. Integer i5 = new Integer(100);
  8. Integer i6 = new Integer(100);
  9. System.out.println( i5 == i6); //false,使用new ,必定创建新对象,地址不一样

注:装箱是Integer.valueOf(int i),拆箱是 i1.intValue()。当然你拆箱时候可以指定返回的数据类型。

JDK源码Integer类中,提供的方法有

  1. @Override
  2. public short shortValue() {
  3. return (short) value;
  4. }
  5. @Override
  6. public long longValue() {
  7. return value;
  8. }

等等。根据需要调用。

其他几个类中的自动装箱拆箱就不再一一列出。类似Integer类。

String类。

String的不可变性

String类是Java中常用的类,我们大家都知道String对象是不可变的,那么这种不可变体现在哪呢。当我们使用

  1. String str = "abc";

这种定义方法时,“abc”会放入常量池中,以后如果再有定义

  1. String str2 = "abc";

时,其实str和str2指向的是常量池中同一个对象。

  1. System.out.println(str == str2); //true,指向同一个 "abc"串

不可变其实指的是串”abc”不可变。 如果对str2做如下改变

  1. str2 = str2 + "de";

系统会在字符串常量池中创建新串 “abcde”,然后str2指向这个新串,而原来的str仍然指向原来的 “abc”。

深入探究String

打开String的JDK源码,能看到很多关于String的具体实现。

  • String类是final的,不可被继承。
  • String类是的本质是字符数组,final char[] value;

创建字符串的方式很多,归纳起来有三类:

  • 使用new关键字创建字符串,比如 String s1 = new String(“abc”);
  • 直接指定。比如 String s2 = “abc”;
  • 使用串联生成新的字符串。比如 String s3 = “ab” + “c”;

String对象的创建也有很多门道,这里把我遇到的困惑列出来。

1、String s1 = new String(“abc”); 创建了几个对象?

实例如下:

  1. String s1= new String("abc");
  2. String ss1 = "abc";
  3. String s2 = new String("abc");
  4. String ss2 ="abc";
  5. System.out.println(s1 == s2); //false
  6. System.out.println(ss1 == ss2); //true

对于s1,对其定义时String pool中没有”abc”,String s1=new String(“abc”)会先后在pool中和heap中定义”abc”,所以它创建了两个对象;而对于s2,它只在heap中定义了一个”abc”,所以它创建了一个对象;而对于ss1和ss2,他们定义的时候pool中已经有了”abc”,所以此时定义了 个对象。

2、用”+”串联字符串

先看例子:

  1. String str = "abc";
  2. String str1 = "ab" + "c";
  3. String str2 = "ab";
  4. String str3 = str2 + "c";
  5. final String str4 = "ab";
  6. String str5 = str4 + "c";
  7. System.out.println(str == str1); //true
  8. System.out.println(str == str3); //false
  9. System.out.println(str == str5); //true

这个刚开始着实让我很费解。

后来经过不断实验和阅读很多博客后,得出如下结论。

  1. String str1 = "abc";
  2. String str1 = "ab" + "c"; 的效果是相同的。因此定义
  3. String str1 = "ab" + "c"; 就相当于定义了
  4. String str1 = "abc";

因此指向String pool中同一个对象。输出为true。

而对于

  1. String str3 = str2 + "c"; JVMjava底层会先创建一个StringBuilder对象,封装str2, 接着再加上“c”,而StringBuildertoString()方法还原一个新的String对象,在堆中开辟一块空间存放此对象。即在堆中重新开辟一块空间存放"abc",而str3指向这个堆中的对象。输出为false

第三个

  1. final String str4 = "ab";
  2. String str5 = str4 + "c"; //相当于 String str5 = "ab" + "c";

当用final修饰后,str4就变为了常量,在常量池中创建”ab”,当执行到String str5 = str4 + “c”;时,编译器直接就把str4当成了”ab”,str5此时就已是”abc”,它指向常量池中的”abc”,所以str和str5指向的是同一个对象,输出为true。

函数调用中对象传参问题

请看下面的例子。

  1. class Sdudent{
  2. String name;
  3. int age;
  4. public Sdudent(String name,int age){
  5. this.name = name;
  6. this.age = age;
  7. }
  8. }
  9. public class Test {
  10. public static void main(String[] args) {
  11. int i = 10;
  12. String str = "java";
  13. StringBuffer sb = new StringBuffer("java");
  14. Sdudent s = new Sdudent("tom",20);
  15. System.out.println("======改变前========");
  16. System.out.println("i = " + i );
  17. System.out.println("str = " + str);
  18. System.out.println("sb = "+sb);
  19. System.out.println("s.name = "+s.name);
  20. System.out.println("s.age = "+s.age );
  21. test(i,str,sb,s);
  22. System.out.println();
  23. System.out.println("======改变后========");
  24. System.out.println("i = " + i );
  25. System.out.println("str = " + str);
  26. System.out.println("sb = "+sb);
  27. System.out.println("s.name = "+s.name);
  28. System.out.println("s.age = "+s.age );
  29. }
  30. public static void test(int i, String str, StringBuffer sb, Sdudent s){
  31. i = 5;
  32. str += " change";
  33. sb.append(" change");
  34. s.name +=" change";
  35. s.age = 30;
  36. }
  37. }

输出结果为:

  1. ======改变前========
  2. i = 10
  3. str = java
  4. sb = java
  5. s.name = tom
  6. s.age = 20
  7. ======改变后========
  8. i = 10
  9. str = java
  10. sb = java change
  11. s.name = tom change
  12. s.age = 30

可以看到由于int是基本数据类型,在函数中改变它的值,并不会影响它原来的值。 你可能认为str传递的是引用,返回后应该会改变才对,其实这里也体现了String对象的不可变性质,函数中str += “ change”; 实质上这个函数中的str已经指向了别的对象,而函数外原来的str仍然指向原来的对象。

StringBuffer和Sdudent类的传递都是引用传递,函数中对它的改变,就会体现在函数外。

总结

比较深入的学习了java中的对象的引用问题。对于以前模糊的不可变,装箱等拆箱概念,有了更深一步的认识。下一篇开始Cloneable接口中,对对象clone的问题。