Java和C++在细节上的差异:程序设计结构

开发 后端
本文主要从程序设计结构、对象与类、继承三个方面讲解了Java和C++在细节上的差异。

一、基本程序设计结构:

Java的基本程序结构、关键字、操作符都和C/C++非常相似,以下为主要的几点区别:

1. Java的原始数值型数据类型中不包含无符号类型,如c中的unsigned int。

2. 在进行移位运算时,当向左边移动时,如1 << 35, 对于int类型,由于其占有4个bytes(32bits), 因此在Java中,大于32的移位将对32取模,即1 << 35的结果等于1 << 3,以此类推,long将会对64取模。对于int类型而言,如果确实需要获取32位以上的移位,需要将返回值的类型提升到long即可。

3. 在c语言中,可以通过判断一个数值是否为0来代替布尔中的false,其他的数值均表示为true。该写法可应用于if和while等子句中,如 if (i) {....}, 当i的值不为0时,该条件可为真,或者是在判断指针对象是否为NULL时,也可作为if和while的条件,因此很容易出现将if (i == 9) {...}写成if (i = 9) {...}的低级错误,在Java中禁止了该类转换,既if和while中条件必须是布尔类型,如果在Java中写成 if (i = 9) {...}将会直接导致编译错误,从而更好的避免了该类问题的发生。

4. Java中去除了goto字句,但是仍然视为保留字。然而Java中的break字句,新增了带标签的break [label],可以使break语句直接跳出指定的循环,而不仅仅是缺省的最内层循环。注:标签必须放在希望跳出的最外层循环之前,并且紧跟一个冒号。如:

  1. public void test() { 
  2.     int n; 
  3.     read_data: 
  4.     while (...) { 
  5.         for (...) { 
  6.             System.out.print("Enter a number >= 0: "); 
  7.             n = in.nextInt(); 
  8.             if (n < 0
  9.                 break read_data; 
  10.         } 
  11.     } 
  12.     //下面的代码将会被立即执行,当break跳出最外层的循环之后。 
  13.     if (n < 0) { 
  14.         ... 
  15.     } else { 
  16.         ... 
  17.     } 

5. Java中支持0长度的数组定义,如int et = new int[0]; 在C/C++中,该写法将会导致编译错误。

6. 多维数组的两种常用访问方式。

  1. public static void main(String[] args) { 
  2.     int[][] magicSquare =  
  3.     { 
  4.         {16,3,2,13}, 
  5.         {5,10,11,8}, 
  6.         {9,6,7,12}, 
  7.         {4,15,14,1
  8.     }; 
  9.     // 通过普通的for循环访问 
  10.     for (int i = 0; i < magicSquare.length; ++i) { 
  11.         for (int j = 0; j < magicSquare[i].length; ++j) { 
  12.             System.out.printf("%s ",magicSquare[i][j]); 
  13.         } 
  14.         System.out.println(); 
  15.     } 
  16.     // 通过普通的for each循环访问 
  17.     for (int[] row : magicSquare) { 
  18.         for (int col : row) { 
  19.             System.out.printf("%s ",col); 
  20.         } 
  21.         System.out.println(); 
  22.     } 
  23. /*两次输出结果均为:         
  24.   16 3 2 13  
  25.   5 10 11 8  
  26.   9 6 7 12  
  27.   4 15 14 1 */ 

7. Java中的不规则二维数组。

  1. public void foo() { 
  2.     int[][] odds = new int[NMAX+1][]; 
  3.     for (int n = 0; n <= NMAX; ++n) 
  4.         odds[n] = new int[n + 1]; 
  5.      
  6.     for (int n = 0; n < odds.length; ++n) { 
  7.         for (int k = 0; k < odds[n].length; ++k) 
  8.             odds[n][k] = n * k; 
  9.     } 

 C/C++中对应于Java的不规则二维数组的表示方式。

  1. void foo() { 
  2.     int** odds = new int*[10]; 
  3.     for (int n = 0; n < 10; ++n) { 
  4.         if (n == 0
  5.             odds[n] = new int
  6.         else 
  7.             odds[n] = new int[n + 1]; 
  8.     } 
  9.          
  10.     for (int n = 0; n < 10; ++n) { 
  11.         for (int k = 0; k < n + 1; ++k) 
  12.             odds[n][k] = n * k; 
  13.     } 
  14.     //注:C/C++代码部分需要自行释放分配的内存。             
  15.     for (int n = 0; n < 10; ++n) { 
  16.         if (n == 0
  17.             delete odds[n]; 
  18.         else 
  19.             delete [] odds[n]; 
  20.     } 
  21.     delete [] odds; 

 二、对象与类:

1. Java对象实例的存储方式:

所有的Java对象实例都是通过new的方式创建的,如Employee employee = new Employee()。而此时创建的employee对象实例,实际是指向Employee对象的一个实例的引用,主要体现为实例之间基于等号的赋值,如:employee = employee2; 赋值后两个变量将指向同一个Employee对象实例。Java处理对象变量的方式和C++中的引用比较类似,但是还是存在一定的差异,首先C++不存在空引用,既引用变量定义时也必须被同时声明其所引用的对象实例,再者就是引用一旦定义时初始化后就不能再被重新赋值了。因此这里可以将Java的对象变量看做C++中的对象指针,如:BirthdayDate d; /*Java*/ 等同于 BirthdayDate* d; /*C++*/。

与Java对象实例声明的方式相同,C++中的对象指针也是通过new的方式进行初始化的,如BirthdayDate* d = new BirthdayDate. 同样可以将C++中的对象指针赋值为NULL,也可以将其重新赋值指向另外一个对象实例。与Java相同,通过new操作符创建的对象实例是存储在堆中的,不同的是,Java的对象在创建后,无需开发人员在去关注该对象实例需要合适被释放,所有的操作均有Java虚拟机中提供的垃圾回收机制自动完成。而C++中的该类对象,则需要开发人员通过调用delete操作符来自行完成释放,如果忘记释放将会产生内存泄露。在C++中,不仅可以将对象存储在堆上,同样也可以定义并存储的栈上,如BrithdayDate d; 该对象实例不需要手工释放,在栈退出时将自动释放该对象的存储空间,同时也会调用该对象的析构函数。

2. Java对象方法的显式参数和隐式参数:

  1. public class Employee { 
  2.     public void raiseSalary(double byPercent) { 
  3.         double raise = salary + byPercent / 100
  4.         salary += raise; 
  5.     } 
  6.     private double salary; 

raiseSalary是Employee类的一个成员方法,该方法是由两个参数构成,一个是显式参数byPercent,另一个则是隐式参数this,既raiseSalary方法是实现体可以改为:

  1. public void raiseSalary(double byPercent) { 
  2.     double raise = this.salary + byPercent / 100
  3.     this.salary += raise; 

这里的隐式参数this表示当前调用raiseSalary方法的对象实例的自身,该机制和C++基本相同。

注:静态方法中不存在该特征。

3. Java对象中定义的final实例域,如:public class Employee { ... private final String name; }, 该类型的field必须在对象构造函数中进行初始化,之后该变量将不能再被重新赋值。和final字段相似,C++对象中的const成员变量也必须在对象构造函数的初始化列表中完成赋值任务,在之后的使用中该字段将不会再被修改,否则会产生编译错误。对于Java的final域而言,以便应用于基本数据类型,如int,double等,或者不可变类型,如String。对于可变类型而言,final修饰符可能会造成某些预料之外的混乱,如 private final Date hiredate; 当该field作为某个get方法的返回值返回给调用者之后,final的修饰作用只能保证返回后的date对象不能再被重新赋值并指向新的对象实例引用,但是可以通过直接修改返回值对象的自身数据来破坏对象的封装性,从而可能造成数据的非法性,或者状态的不一致性。

4. 函数参数传递的方式:传值和传引用。

在Java中调用函数是,参数都是通过传值的方式传递到函数内部,然而根据参数类型的不同,其表现仍然存在一定的差异。主要总结为以下3点:

1)被调用方法不能修改一个基本数据类型的参数,如:int,double,boolean等,见如下代码:

  1. private static void tripleValue(double x) { 
  2.     x *= 3
  3.     System.out.println("End of method: x = " + x); 
  4.  
  5. public static void testTripleValue() { 
  6.     System.out.println("Test tripleValue"); 
  7.     double percent = 10
  8.     System.out.println("Before: percent = " + percent); 
  9.     tripleValue(percent); 
  10.     System.out.println("After: percent = " + percent); 
  11. /*    结果如下: 
  12.     Test tripleValue 
  13.     Before: percent = 10.0 
  14.     End of method: x = 30.0 
  15.     After: percent = 10.0  */ 

2)被调用方法可以改变一个对象参数的状态,见如下代码:

  1. private static void tripleSalary(Employee x) { 
  2.     x.raiseSalary(200); 
  3.     System.out.println("End of method: salary = " + x.getSalary()); 
  4.  
  5. public static void testTripleSalary() { 
  6.     System.out.println("Test tripleSalary"); 
  7.     Employee harry = new Employee("Harry",50000); 
  8.     System.out.println("Before: salary = " + harry.getSalary()); 
  9.     tripleSalary(harry); 
  10.     System.out.println("After: salary = " + harry.getSalary()); 
  11. /*    结果如下: 
  12.     Test tripleSalary 
  13.     Before: salary = 50000.0 
  14.     End of method: x = 150000.0 
  15.     After: salary = 150000.0  */ 

3)被调用方法不能实现让对象参数引用一个新的对象,见如下代码:

  1. private static void swap(Employee a,Employee b) { 
  2.     Employee temp = x; 
  3.     x = y; 
  4.     y = temp; 
  5.     System.out.println("End of method: x = " + x.getName()); 
  6.     System.out.println("End of method: y = " + y.getName()); 
  7. }  
  8. public static void testSwap() { 
  9.     System.out.println("Test Swap"); 
  10.     Employee a = new Employee("Alice",70000); 
  11.     Employee b = new Employee("Bob",60000); 
  12.     System.out.println("Before: a = " + a.getName()); 
  13.     System.out.println("Before: b = " + b.getName()); 
  14.     swap(a,b); 
  15.     System.out.println("After: a = " + a.getName()); 
  16.     System.out.println("After: b = " + b.getName()); 
  17. /*    结果如下: 
  18.     Test swap 
  19.     Before: a = Alice 
  20.     Before: b = Bob 
  21.     End of method: x = Bob 
  22.     End of method: y = Alice 
  23.     After: a = Alice 
  24.     After: b = Bob     */ 

C++有值调用和引用调用,引用参数标有&符号。如:void tripleValue(double& x)或void swap(Employee& x,Employee& y)方法实现修改他们引用参数的目的,既该方法执行完成后,调用函数的参数变量的值将发生改变。

5. 对象的构造和构造函数:

在Java中如果一个class没有定义任何构造函数,Java编译器将自动生成一个缺省的构造函数,没有任何参数,其行为只是按照Java默认的方式初始化该类的所有域变量,如数值型为0,布尔为false,对象则为null。但是如果该class定义了自己的构造函数,那么缺省构造函数将不会被自动生成,再试图调用自动生成的缺省构造函数将会导致编译错误。该行为和C++完全一致。但是Java提供了另外一种域变量初始化方式,如下:

  1. public class Employee { 
  2.     ... 
  3.     private String name = "";    //直接赋值 
  4.     private int id = assignId();//通过调用域方法完成初始化。 

在C++中不能直接在类的定义中以任何形式直接初始化成员变量。但是C++提供了在构造函数中以初始化列表的方式完成成员变量对象的初始化,特别是const成员,必须在这里赋值。

通过一个构造器调用另一个构造器从而完成域变量的初始化和部分代码复用。通过this关键字(或称隐式参数)作为函数名,然后传入参数调用你期望的另一个构造函数,注:this被调用之前不能执行任何其他的code。

  1. public Employee(double s) { 
  2.     //calls Employee(String,double) 
  3.     this("Employee #" + nextId,s); 
  4.     ++nextId; 

在C++中如果打算完成此功能,必须将构造函数的部分逻辑抽取出来,以便让多个构造函数去调用,然后不同的构造函数之间不能直接调用。

在Java定义的子类中,如果子类的构造函数不是调用父类的缺省构造函数,则需要在子类构造函数的***行代码中指定欲调用的父类构造函数,该调用需要通过super关键字来完成。见如下代码:

  1. public class MyFirst { 
  2.     public static void main(String[] args) { 
  3.         BaseClass bc1 = new SonClass(); 
  4.         BaseClass bc2 = new SonClass(5); 
  5.     } 
  6.  
  7. class BaseClass { 
  8.     public BaseClass() { 
  9.         System.out.println("This is BaseClass"); 
  10.     } 
  11.      
  12.     public BaseClass(int i) { 
  13.         System.out.println("This is BaseClass with i."); 
  14.     } 
  15.  
  16. class SonClass extends BaseClass { 
  17.     public SonClass() { 
  18.         System.out.println("This is SonClass"); 
  19.     } 
  20.      
  21.     public SonClass(int i) { 
  22.         super(5); 
  23.         System.out.println("This is SonClass with i"); 
  24.     } 
  25. /*    结果如下: 
  26.     This is BaseClass 
  27.     This is SonClass 
  28.     This is BaseClass with i. 
  29.     This is SonClass with i */ 

在C++中也可以完成该种类型的指定,但是必须在子类构造函数的初始化列表中完成对父类指定构造函数的调用。

  1. class BaseClass { 
  2. public
  3.     BaseClass() { 
  4.         printf("This is BaseClass\n"); 
  5.     } 
  6.  
  7.     BaseClass(int i) { 
  8.         printf("This is BaseClass with i\n"); 
  9.     } 
  10. }; 
  11.  
  12. class SonClass : public BaseClass { 
  13. public
  14.     SonClass() { 
  15.         printf("This is SonClass\n"); 
  16.     } 
  17.  
  18.     SonClass(int i) : BaseClass(i) { 
  19.         printf("This is SonClass with i\n"); 
  20.     } 
  21. }; 
  22.  
  23. int main() 
  24.     BaseClass* bc1 = new SonClass; 
  25.     BaseClass* bc2 = new SonClass(5); 
  26.     delete bc1; 
  27.     delete bc2; 
  28.     return 0
  29. /*    结果如下: 
  30.     This is BaseClass 
  31.     This is SonClass 
  32.     This is BaseClass with i. 
  33.     This is SonClass with i */ 

在Java的域变量初始化方法中存在初始化块的方式,既除声明即初始化、构造函数初始化之外的第三种域变量初始化方式。在一个类的声明中可以存在多个代码块,只要构造类的对象,这些块就会被执行,然后再运行类的构造函数。静态域变量可以在静态初始化块中完成初始化的工作,但是该初始化块只是在类***次加载时被执行一次,之后都将不再被执行。见如下代码:

  1. class Employee { 
  2.          public Employee(String n,double s) { 
  3.              name = n; 
  4.              salary = s; 
  5.          } 
  6.           
  7.          ... 
  8.           
  9.          private static int nextId; 
  10.          private int id; 
  11.          private String name; 
  12.          private double salary; 
  13.           
  14.          //object initialization block. 
  15.          { 
  16.              id = nextId; 
  17.              nextId++; 
  18.          } 
  19.           
  20.          //static initialization block. 
  21.          static 
  22.          { 
  23.              Random generator = new Random(); 
  24.              nextId = generator.nextInt(); 
  25.          } 
  26.      } 

6. C++的对象析构和Java对象的finalize方法:

C++是有显式的析构方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构函数中,最常见的操作时回收分配给对象的存储空间,系统资源等。有Java有自动的垃圾回收器,不需要人工回收内存,所以Java并不支持析构函数。如果打算在Java的代码中完成类似的工作,可以通过为该类添加finalize方法,该方法将会在垃圾收集器清除对象之前调用,在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能调用。如果某个资源确实需要在使用完毕后立刻关闭,那么就需要由人工来管理。可以应用一个类似dispose或close的方法完成相应的清理操作。特别需要说明,如果一个类使用了这样的方法,当对象不再被使用时一定要调用它。

7. Java的包 vs C++的名字空间

他们具有极为相同的只能,即防止名字污染。当一个应用程序中存在多个第三方组件,那么不同组件中命名了相同名称的类将是极为可能发生的,如Java中的Date类,在java.util和java.sql中均存在该名称的类的声明。为了有效的防止名字污染,C++中采用了namespace和using namespace的指令来明确定义某个类具体所位于的具体位置,Java中则采用了package和import语句。
Java在Java SE5.0 开始,import语句不仅可以导入类,还增加了导入静态方法和静态域的功能。如import static java.lang.System.*。在完成该静态导入之后,就可以在剩下的代码中直接使用System类的静态方法和静态域了,如out.println();exit(0)。该技巧主要用于带有较长名称的常量,如if (d.get(DAY_OF_WEEK) == MONDAY) ...,看起来比if (d.get(Calendar.DAY_OF_WEEK) == Calendar.MONDAY) ...要容易的多。

#p#

三、继承:

1. Java和C++在对象继承方面的主要差异:

对象的继承性是所有面向对象语言都支持的面向对象特性之一,Java和C++作为两个重要的面向对象开发语言在此方面有着较多的相似性,但是在有些概念的表示方式上还是存在着一定的差异,先列举如下:

1) 对象继承的关键字,Java中采用extents关键字,如class DeriveClass extends BaseClass, 在C++中则使用(:)冒号表示类之间的继承,如class DeriveClass : public BaseClass。

2) Java的继承方式中不存在public,protected和private,其表现行为和C++中的public继承完全一致。

3) 在有些情况下,子类中的方法需要显式的调用超类中的方法实现,特别是当子类中也存在同样方法签名的实现时,如果没有明确的指出需要调用超类的方法,Java的编译器会将子类当前的方法列为本次调用的候选方法,见如下代码:

  1. class DeriveClass extends BaseClass { 
  2.     public double getSalary() { 
  3.         double baseSalary = getSalary(); 
  4.         return baseSalary + bonus; 
  5.     } 

以上代码中的getSalary()方法将会递归的调用其自身,而开发者的实际用意是调用超类中的getSalary方法,由于超类和子类中具有相同签名的该方法,因此编译器在此时选择了子类中的getSalary。其修改方式如下:

  1. class DeriveClass extends BaseClass { 
  2.     public double getSalary() { 
  3.         double baseSalary = super.getSalary(); 
  4.         return baseSalary + bonus; 
  5.     } 

加上关键字super明确的指出要调用超类中的getSalary方法。在C++中的实现方式为BaseClass::getSalary(),既在方法签名的前面加上父类的名字和两个连在一起的冒号(::)。

  1. class DeriveClass : public BaseClass { 
  2. public
  3.     double getSalary() { 
  4.         double baseSalary = BaseClass::getSalary(); 
  5.         return baseSalary + bonus; 
  6.     } 

4) Java中所有未声明为final的方法都视为可以继承的虚方法。在C++中,尽管没有此类限制,但是在实际的应用中还是存在一些潜在的技巧以达到此效果。对于C++类中声明的公有成员方法,如果该方法未声明为virtual,既虚函数,则暗示该类的子类实现者不要在子类中覆盖(override)该方法。

5) Java中不支持多重继承,不仅有效的避免了C++因多重继承而带来的一些负面影响,与此同时,在Java中可以通过继承(extends)单个父类和实现(implements)多个接口的方式更好表达该类设计意愿。

6) Java中如果子类和超类同时包含具有相同签名的公有域方法,那么在子类中将覆盖超类中的域方法。这其中的方法签名只是包括方法名和参数列表,既参数的个数和类型,函数的返回值不包含在方法签名中,但是在Java中针对该种方法覆盖的返回值还是存在一定的限制,既子类中的返回值的类型,或者与超类中该方法的返回值类型相同,或者为其返回类型的子类。C++中没有此类返回值类型的限制。但是Java的此类限制也会带来一些潜在的迷惑和危险,见如下代码:

  1. class Employee { 
  2.     public Employee[] getBuddies() { ... } 
  3.  
  4. class Manager extends Employee { 
  5.     public Manager[] getBuddies() { ... } 
  6.  
  7. public static void main(String[] args) { 
  8.     Employee[] m = new Manager().getBuddies(); 
  9.     //在Java中子类的数组在复制给超类的数组时不需要显式的转换,就像 
  10.     //子类的实例赋值给超类的实例一样,也不需要任何显式的转换。 
  11.     //赋值之后e和m指向相同的内存地址,同样e[0]和m[0]也指向相同的实例。 
  12.     Employee[] e = m; 
  13.     //本次赋值合法也不会引发任何异常,但是会导致一个潜在的问题,既 
  14.     //m[0]的对象已经被悄悄的改变了,指向了Employee的另外一个子类。 
  15.     e[0] = new OtherEmployee(); 
  16.     //此时再调用m[0]中Manager定义的域方法时将会引发Java的运行时异常。 
  17.     m[0].setBonus(1000); 

7) Java中的final类,如果某个自定义类型被加入final关键字,则表示该类将不能被继承,否则会直接产生编译错误。在C++中没有特殊的关键字类完成此类限制,然而在实际的应用中也同样存在一些潜在的技巧协助开发者来进行此类限制的甄别。如将父类中的析构函数不设置为虚函数,此方法则间接的暗示子类的实现者要留意,如果仍然继承该父类,那么在实现多态时,如BaseClass* c = new DeriveClass,如果之后需要释放c变量的内存资源时 delete c, 此时由于父类中的析构函数并不是虚函数,因此此次调用将只会执行父类的析构函数,而不会调用子类的析构函数,最终导致类分割所带来的一些潜在错误或资源泄漏。

8) 内联方法,在C++中有特殊的关键字inline用于帮助编译器来推断是否需要将该方法编译成内联方法,以提高运行时的效率。在Java中没有此类关键字,而是通过编译器的一连串推演,最终决定该域方法是否可以编译成内联方法,主要候选方法为简短、被频繁调用且没有真正被子类覆盖的域方法。

9) 超类到子类的强制类型转换。在Java中可以通过直接强转的方式来转换,如Manager m = (Manager)e。如果装换失败将会引发运行时异常ClassCastException,因此很多情况下为了避免此类异常的发生,需要在强转之前先进行判断,如if (e instanceof Manager) { ... }, 如果条件为真,装换将顺利完成。在C++中也可以采用这样的直接强转方法,但是即使类型不匹配程序也不会在强转是引发任何异常,而是在后面针对该变量的使用时才会导致错误的发生。在C++中存在dynamic_cast关键字,如dynamic_cast<Manager*>和dynamic_cast<Manager&>,前者为基于指针的转换,如果转换失败返回变量为NULL,而后者则会引发异常。

10) 抽象类:在Java中如果class被定义为abstract class,该类将不能被实例化,如果子类未能完全实现超类中所有的抽象方法,那么子类也将会被视为抽象类。C++中没有特殊的关键字来表示抽象类,而且通过将类中的一个或多个方法定义为纯虚方法来间接实现的,见如下C++代码,其中的first和second均为纯虚方法,既在方法的尾部添加" = 0 "。

  1. class AbstractClass { 
  2. public
  3.     virtual void first() = 0
  4.     virtual void second() = 0
  5.     virtual void third(); 

11) protected关键字在Java和C++中针对域方法和域字段的访问方式存在着不同的限制级别,相同之处是protected的方法和字段都可以被子类直接访问,不同之处是Java中相同包中的类也可以直接他们。C++自身并不存在包的概念,然而即便是相同名字空间内的对象也不能直接访问。

2. Object:

Java是单根结构的框架,所有的对象都是Object的子类,即使在对象声明时没有进行直接的指定,Java的编译器将会自行搞定这些。C++中没有适当的类作为所有对象的根类,然而在有些类库中可以自行定义,如MFC的CObject等。Java的Object中有3个非常重要的方法equals、hashCode和toString。如果子类中重载了他们中的任意一个方法,同时也建议重载另外两个域方法。

1) equals: 主要用于判定两个对象是否相等。类的实现者可以根据自己的真实逻辑来重新实现该方法,通用实现规则见下例:

  1. public class Employee { 
  2.     //1. 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。 
  3.     public boolean equals(Object otherObject) { 
  4.         //2. 检测this与otherObject是否引用同一个对象(一种优化) 
  5.         if (this == otherObject) 
  6.             return true
  7.         //3. 检测otherObject是否为null,如果null,则返回false。 
  8.         if (otherObject == null
  9.             return false
  10.         //4. 比较this与otherObject是否属于同一个类。 
  11.         //如果子类中的equals语义各不相同,使用下面的getClass方式,精确定义类类型。 
  12.         if (getClass() != otherObject.getClass()) 
  13.             return false
  14.         //如果子类中的equal语义和超类完全相同,可以使用instanceof检测即可。 
  15.         //5. 将otherObject转换为相应的类类型变量 
  16.         Employee other = (Employee)otherObject; 
  17.         //6. 现在开始对所有需要比较的域进行比较了。其中使用==比较基本类型, 
  18.     //使用equals比较对象类型。 
  19.         return name.equals(other.name) && salary == other.salary; 
  20.     } 

注:数组元素的比较可以调用Arrays.equals方法检测。如果子类中重新定义了equals方法,就要在其中包含调用super.equals(other).

Java在语言规范中给出了自定义equals方法需要遵守的规则:

◆  自反性: 对于任何非空引用x,x.equals(x)应该返回true。

◆  对称性: 对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。

◆  传递性: 对于任何引用x,y和z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也应该返回true。

◆  一致性: 如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。

对于任意非空引用x,x.equals(null)应该返回false。

2) hashCode: 导出一个经过哈希计算后的整型值,Java对hashCode的缺省实现是返回当前对象的存储地址。一下列出String的hashCode实现方式:

  1. public int hashCode() { 
  2.     int hash = 0
  3.     for (int i = 0; i < length(); ++i) 
  4.         hash = 31 * hash + charAt(i); 
  5.     return hash; 

注:自定义类型的equals和hashCode定义必须一致,如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。如果打算实现自定义的hashCode方法,推荐使用在对象构造初始化后就不会再改变的域字段作为hashCode的计算因子。否则一旦使用可变域资源作为hashCode计算因子的一部分,将会导致一些隐藏的问题。比如当Employee对象实例存入HashMap中,但是使用者在存入集合之后,修改了某个参数hashCode计算的域字段的值,此后再在HashMap中查找原有对象时由于hashCode已经改变,因此即使该对象已经存入HashMap中,结果是仍然无法找到最初存入的对象了。数组类型的hashCode,可以通过Arrays.hashCode方法计算得出。

3) toString: Java中比较推荐的实现方式为:

  1. public String toString() { 
  2.     return getClass().getName() +  
  3.         "field1 = " + field1 + 
  4.         "field2 = " + field2; 

注:C#的Framework中也存在一个类似的Object对象,作为C#所有对象(包括自定义对象)的唯一根类,其中也有对应的3个方法equals、hashCode和toString。Effective C#中针对这3个方法提供了一个很好的建议,既如果自定义类重载了这3个方法中任何一个,那么强烈建议该类也重载另外两个域方法。如对equals和toString而言,如果x.equals(y)返回true,那么x.toString.equals(y.toString)也将返回true,反之亦然。针对equals和hashCode域方法还有一种推荐的实现方式,如下:

  1. public bool equals(Object other) { 
  2.     return toString().equals(other.toString()); 
  3.  
  4. public int hashCode() { 
  5.     return toString().hashCode(); 

3. 包装类和自动打包:

1) 包装器对象均为不可变对象,如String,既一旦初始化之后其值将不会再被改变。包装器类是final类,不能为继承。

2) 自动拆包和打包:Integer n = 3; n++; 在执行n++时,Java编译器将自动插入一条拆包指令,然后进行自增计算,***再将结果打入对象包内。

3) 自动打包的规范要求boolean, byte, char <= 127, 和介于-128--127之间的short和int被包装到固定的对象中,见如下代码:

  1. public void test() { 
  2.     Integer a1 = 1000
  3.     Ingeger a2 = 1000
  4.     if (a1 == a2) 
  5.         System.out.println( 
  6.             "This won't be printed out because they are greater than 127."); 
  7.  
  8.     Integer a3 = 100
  9.     Ingeger a4 = 100
  10.     if (a3 == a4) 
  11.         System.out.println( 
  12.             "This will be printed out because they are less then 127."); 

4) 打包和拆包过程是编译器行为,不是虚拟机行为,是编译器在生成字节码的时候自动插入的指令。

5) 包装类在容器中的应用。对于Java提供的泛型容器类其类型参数不能是primitive type,如int、float等,如果确实需要添加类似的数据,需要将相应的包装类作为容器类型参数,之后在插入原始类型数据,但是在插入过程中Java的编译器将自动插入打包指令,因此实际插入到容器中的仍然是包装类对象,见如下代码:

  1. public static void main(String args[]) { 
  2.     ArrayList<Integer> l = new ArrayList<Integer>(); 
  3.     for (int i = 0; i < 10; ++i) 
  4.         l.add(i); 
  5.      
  6.     for (int i = 0; i < l.size(); ++i) { 
  7.         System.out.printf("The value is %d.\t",l.get(i)); 
  8.         System.out.printf("The class name is %s.\n" 
  9.             , l.get(i).getClass().getName()); 
  10.     } 
  11. /*    结果如下: 
  12.     The value is 0.    The class name is java.lang.Integer. 
  13.     The value is 1.    The class name is java.lang.Integer. 
  14.     The value is 2.    The class name is java.lang.Integer. 
  15.     The value is 3.    The class name is java.lang.Integer. 
  16.     The value is 4.    The class name is java.lang.Integer. 
  17.     The value is 5.    The class name is java.lang.Integer. 
  18.     The value is 6.    The class name is java.lang.Integer. 
  19.     The value is 7.    The class name is java.lang.Integer. 
  20.     The value is 8.    The class name is java.lang.Integer. 
  21.     The value is 9.    The class name is java.lang.Integer. 
  22. */ 

4. Java函数的变参表示方式:

PrintStream printf(String fmt,Object...args),其效果相当于 PrintStream printf(String fmt,Object[] args)。在C++中变参的表示方式为int printf(const char* fmt, ...); 其后的缺省参数需要通过C语言中提供的宏VA_LIST来协助完成。

原文链接:http://www.cnblogs.com/stephen-liu74/archive/2011/07/27/2118660.html

【系列文章】

  1. Java和C++在细节上的差异:枚举与反射
  2. Java和C++在细节上的差异:接口与内部类
  3. Java和C++在细节上的差异:泛型程序设计
责任编辑:林师授 来源: Stephen_Liu的博客
相关推荐

2011-12-06 12:16:58

Java

2011-12-06 10:48:32

Java

2011-12-06 11:12:59

Java

2010-01-28 09:54:27

C++程序设计

2010-01-13 18:30:18

CC++程序设计

2012-11-08 09:49:30

C++Java程序员

2010-01-27 14:24:15

C++程序设计

2009-06-01 08:48:19

作用域变量作用域对象作用域

2011-07-22 13:41:57

java

2011-07-10 15:36:54

C++

2010-01-11 17:43:23

C++程序设计

2010-01-11 10:34:22

C++程序

2010-01-08 16:10:59

C++语言

2011-04-11 10:44:53

对象编程C++

2010-01-11 17:22:02

2009-10-12 13:10:58

马尔可夫链

2011-08-05 15:46:32

Objective-C 程序设计

2017-10-27 13:30:59

大数据MongoDBeBay

2010-01-21 16:45:02

C++设计目标

2009-09-02 13:22:23

C#组件化程序设计
点赞
收藏

51CTO技术栈公众号