一、Object:所有类的超类
Object类是所有类的始祖。如果某个类没有明确地指出超类,则认为Object就是这个类的超类。
可以使用Object类型的变量引用任何类型的对象
Object obj=new StringBuilder();
但是Object类型的变量只能用于作为各种值的通用持有者,想要对其的内容及逆行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换。
Manager aManager=(Manager) obj;
在java中只有基本数据类型(primitive)的值不是对象。所有的数组类型,不管是对象数组还是基本数据类型的数组都扩展了Object类。如下写法均正确:
Manager[] managers=new Manager[10];
obj=managers;//正确
obj=new int[10];//正确
(1)equals方法
//先列出这两种equals静态方法
/*
* java.util.Object中的一个静态方法,使用Object.equals(a,b)访问
* 检测如果a和b都为null,则返回true,如果只是其中一个为null,则返回false,否则调用a.equals(b);方法进一步判断
*/
static boolean equals (Object a,Object b);
/*
* java.util.Arrays中的一个静态方法,使用Array.equals(a,b)访问
* 如果两个数组长度相同,并且在对应的位置上数据元素也都相同,将返回true。
* 数组的元素类型可以是:Object、int、long、short、char、byte、boolean、float、或者double。
*/
static boolean equals(type[] a,type[] b );
Object中的equals方法用于检测一个对象是否等于另外一个对象。
分析相等的情况:
- 两个对象变量引用了同一个对象,则一定相等。
- 两个对象变量引用用了同一个类中的两个对象,但是两个对象的域完全相同,则认为相等。
为某个类编写完美的equals方法的建议:
- equals方法声明的显式参数应该为Object类,这样就覆盖了Object类中的equals方法,否则就是定义了一个完全无关的方法。为了避免发生类型错误,可以使用@Override对覆盖超类的方法进行标记,使用该标记后,如果没有对超类的方法覆盖而是定义了一个新的方法,编译器就会给出错误报告。(后面的步骤均在该equals方法中书写)
//正确代码如下:
@Override public boolean equals(Object otherObject){
...
}
//如果将Object换成Manager或者其他类型,编译器就会报错
- 检测this与otherObject是否引用了同一个对象
/*
* 判断两个对象变量是否相等,首先可以判断两个对象变量是否引用了同一个对象,如果是,则肯定相等。否则,接着进行后面的判断。
* 这其实是一个优化,也是经常使用的方式,因为计算这个等式要比一个一个比较类中的域所付出的代价小很多。
*/
if (this==otherObject) return true;
- 检测otherObject是否为null
/*
* 判断otherObject是否为null其实很有必要,如果为null,则直接返回false,否则接着后面的判断
*/
if(otherObject==null)return false;
- 比较this域otherObject是否同属于同一个类
/*
* 若两者不属于同一个类,则直接返回false,没有必要进行后面的判断
*
* 这里可以依据两种不同的情况,使用不同的方法判断
*/
/* 1、
* 如果equals的予以在每个类上都有所改动,则使用过getClass()方法检测
* 即:若子类都拥有自己的相等概念,则根据对称性需求,必须使用getClass进行检测
*/
if(getClass() != otherObject.getClass() )return false;
/*2、
* 如果所有的子类都拥有统一的语义,就是用instanceof检测
* 即:如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。
*/
if ( ! (otherObject instanceof ClassName ))return false;
//这里的ClassName就是目前定义这个equals方法的类
补充instanceof讲解:
- getClass方法将返回一个对象所属的类
- instanceof是一个java中的二元运算符(关键字),用于判断其左边的对象是否为其右边类的实例,即:指出这个对象是否是这个特定类或则他的子类的一个实例。返回的是boolean类型
//例如:判断aManager是否是Manager类的实例
if (aManager instanceof Manager) return true;
继续编写equals方法:
- 将otherObject转换成相应的类的类型变量
//进行这个转换是因为,经过上述判断,可以得知,这个otherObject和this是属于同一个类的两个对象,接下来就需要判断这两个对象的域值是否相等,所以需要将otherObject转换成this的类型。
ClassName other=(ClassName) otherObject;
- 最后,经过以上的步骤后,就可以直接对两个对象的域进行比较了。使用==比较基本数据类型域,使用equals比较对象域。如果所有域都匹配,就返回true,否则返回false。
/*
* 说明:
* 1、由于只有基本数据类型的值不是对象,所以可以使用==判断是否相等
* 2、对于对象域,或者数组,需要使用Object.equals(a,b);判断这是为了防止a和b都为null的情况,只有两个都不为null,就会自动调用a.equals(b);进行判断
* 3、对于数组类型的域,还可以使用静态的Array.equals(a,b);方法检测相应数组元素是否相等。
*/
return field1==other.field1//这里比较基本数据类型
&& Object.equals( field2, other.field2 )//这里比较对象域
&& ...;
【注意】
如果在需要在上述类的子类中重新定义equals就要在其中包含super.equals(other),并且依旧需要覆盖Object的equals方法。
/*
* 包含super.equals(other)是因为可以先判断两个对象的超类域是否相等,如果相等就接着判断子类中新增的域。
* 例如:假设Manager类已经重写了equals,现在重写它的子类Staff类的equals
*/
public class Staff extends Manager{
@Override
pubilc boolean equals(Object otherObject){
if(! super.equals(otherObject)) return false;
Staff other=(Staff) otherObject;//将其转换为this所属的类,进行如下的实例域判断
return staffField1==other.staffField1
&& Object.equals(staffField2,other.staffField2)
&& ...;
}
}
(2)hashCode方法
hashCode(散列码)是由对象导出的无规律的一个整型值。由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为该对象的储存地址。
一个类可以重新定义hashCode方法,但是如果没有重新定义,则使用的是Object中默认的hashCode方法。
String类型的散列码是有其内容导出的,如果两个字符串的内容是相同的,则他们的散列码就是相同的。
【注意】
- 如果重新定义了equals方法,则必须重新定义hashCode方法,以便用户可以将对象插入到散列表中!!!
- equals方法与hashCode方法的定义一定要相同。即:如果a.equals(b);结果为true,则a.hashCode(b)和b.equals(a)的值一定要相等。
常用的hashCode方法:
/*
* java.uitl.Object(non-Javadoc)
* 返回对象的散列码。散列码可以任意的而证书,包括正数或负数。两个相等的对象要求返回相等的散列码。
*/
int hashCode()
/*
* java.util.Objects
*/
//根据参数所提供的所有对象,返回所有对象的组合散列码
static int hash(Object... objects)
//如果a为null返回0,否则返回a.hashCode(),该方法null安全
static int hashCode(Object a)
/*
* java.lang.(Integer|Long|Short|Double|Float|character|Boolean)
* 返回给定值的散列码
*/
static int hashCode( (int|long|short|byte|double|float|char|boolean) value)
/*
* java.util.Arrays
* 计算数组a的散列码。组成这个数组的元素类型可以是object,int,long,short,char,byte,boolean,float或者double
* 这个散列码由数组元素的散列码组成
*/
static int hashCode(type[] a)
类中重新定义hashCode方法:
规则:hashCode方法应该返回一个整型数值(也可以是负数),并合理的组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。
例如:
public class Manager{
private String name;
private double salary;
private LocalDate hireDay;
public int hashCode(){
return 7* name.hashCode()
+ 11* new Double(salary).hashCode()
+ 13* hireDay.hashCode()
}
}
/*
* 这里的hashCode都是静态方法,都是使用 【对象.方法名 】 的形式访问。
* 由于salary是double基本类型,没有对象,所以salary只是一个double值,故这里使用了new Double().hashCode()新建了一个对象访问hashCode方法
*/
优化:
由于除了基本类型以外,其他类型对象都允许有null,所以可以使用Object.hashCode(Object a);静态方法方法替换上述直接用对象访问hashCode()。如果对象为null则返回0,否则返回【a.hashCode()】
其次可以使用静态方法Double.hashCode(double a);来获取a的散列码,而避免了重新创建Double对象。
即:
public class Manager{
private String name;
private double salary;
private LocalDate hireDay;
public int hashCode(){
return 7* Object.hashCode(name)
+ 11* Double.hashCode(salary)
+ 13* Object.hashCode(hireDay)
}
}
再优化:
当需要组合多个散列值时,可以调用Object.hash并提供多个参数。这个方法会针对各个参数调用Object.hashCode,组合这些散列值并返回。
即:
public int hashCode(){
return Object.hash(name,salary,hireDay);
}
(3)toString方法
toString方法用于返回表示对象值得字符串。大多数toString都遵循这种格式:类名+ [ + 域值 + ]
其中类名可以通过getClass().getName()的形式获取。
toString方法可以供子类使用:设计子类的时,可以自定义一个toString方法,形如:super.toString() + [ + 子类新增域值 + ]
提示:只要对象与一个字符串通过操作符 “ + ” 连接在一起,java编译器就会自动调用toString方法,以便获取这个对象的字符串描述。所以,可以将 x.toString() 替换成 ""+x 。""+x 语句表示将一个空串与x的字符串相连接,这里的x就相当于x.toString()。但是,如果x是基本类型,""+x照样能够执行,而toString方法则需要变换一下。
其实System.out.println(x);中println方法就是直接调用x.toString()并打印字符串。
Object类定义了toString方法,用于打印输出对象所属的类名和散列码。(类名和散列码之间使用@连接)如果一个类没有覆盖Object的toString方法,则都会返回 类名@散列码 。例如数组就继承了Object的toString方法:
int[] singleNumber = {1,3,5,7,9};
String s="" + singleNumber;
/*
*将得到字符串s [I@3c679bde
*其中前缀 [I 表明是一个整形数组
*/
修正方式是使用Arrays.toString(s)静态方法,如果想要打印多维数组,则使用Arrays.deepToString(s)方法。如:
int[] singleNumber = {1,3,5,7,9};
String s=Arrays.toString(singleNumber);
//则s将得到 [1, 3, 5, 7, 9]
提示:强烈建议为每个类增加一个toString方法,它会在调试或者日志中起到很大的作用。
二、对象包装器与自动装箱
所有基本类型都有一个与之对应的类,这些类称为包装器。
Integer、Long、Float、Double、Short、Byte、Character、Void和Booolean。其中前六个类都派生于Number类。
对象包装器是不可变的,并且还是final。即一旦构造了包装器就不允许改变包装在其中的值,并且不可以定义这些类的子类。
由基本数据类型->包装器:自动装箱
由包装器->基本数据类型:自动拆箱
//自动装箱:
list.add(3);
//该调用将自动变成
list.add(Integer.valueof(3));
//自动拆箱:
//如下,将一个Integer对象赋值为一个int值时,就会自动拆箱,即
int n=list.get(i);
//编译器会自动执行如下语句:
int n=list.ger(i).intValue();
//在算术表达式中也常用到拆装箱:例如可以将一个自增操作符应于包装器上
Integer n=3;
n++;
//编译器将自动地插入一条对象拆箱的指令,然后进行自增计算,最后将结果装箱。
【注意】
(1)自动装箱规范要求:boolean、byte、char小于等于127以及介于-128~127之间的short和int被包到固定的对象中。
即:
Integer a=100;
Integer b=100;
if(a==b)return true;
/*
* 由前面讲equals方法时得知,==操作符将比较两个对象变量的引用是否是同一个,而不是比较域值。所以判断相等的时候不建议使用==操作符。
* 但是这里有一个特例。由上述的【注意】可知,如果a=100以及b=100时,a和b将被包装到同一个Integer对象中,此时使用if(a==b)是能够成功判断的。
* 但是如果超出了规定的范围,则==依旧不适用
*/
(2)由于每个之分别包装到对象中,所以在构建集合时候其效率远远低于基本类型的数组。建议使用构建小型集合,因为程序操作的方便性要比执行效率更加重要。
(3)由于装箱器引用可以为null,所以自动装箱有可能会抛出一个NullPointerException异常:
Intrger n=null;
System.out.println(2*n);
//Throws NullPointerException
(4)如果在一个条件表达式中混合使用Integer和Double类型,Integer值会拆箱,转换为double,在装箱为Double:
Integer n=1;
Double x=2.0;
System.out.println(true ? n:x);
//输出1.0
(5)装箱和拆箱是编译器认可的,而不是虚拟机。编译器生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
使用数值对象包装器有一定的好处:可以将某些基本方法放置在包装器中:
/*
* java.lang.Integer
*/
int intValue()
//以int的形式返回Integer对像的值(在Number中覆盖了intValue方法)
static String toString(int a)
//一个新String的形式返回给定数值a的十进制表示
static int toString(int a,int radix)
//返回数值a的基于radix参数进制的表示
static int parseInt(String s)
static int parseInt(String s,int radix)
//返回字符串s表示的整型数值,方法1为十进制整型数值,方法2为radix进制整型数值
static Integer valueOf(String s)
static Integer valueOf(String s,int radix)
//返回用字符串s表示的整型数值进行初始化后的一个新的Integer对象。方法1为十进制整型数值,方法2为radix进制整型数值
【注意】包装器类不可以用来实现修改数值参数的方法。因为Java是按值传递,尽管是包装器,也是传递的引用值。其次包装器是不可变的:包含在包装器中的内容不会改变。所以不能使用包装器类创建修改数值参数的方法。
如果想要编写一个修改数值参数的方法,需要使用org.omg.CORBA包中定义的holder(持有者)类型,包括IntHolder、BooleanHolder等。每个持有类型都会包含一个共有域值(!),通过它可以访问储存在其中的值。如:
public static void tripe(IntHolder x){
x.value=3*x.value;
}
三、参数数量可变的方法
其实printf方法就是一个参数可变的方法。它的实际定义如下:
public class PrintStream{
public PrintStream printf(String fmt,Object...args){
return format(fmt,args);
}
}
上面的...省略号是java代码的一部分,它表明这个方法可以接收任意数量的对象(除了fmt以外的参数)。实际上printf方法接收了两个参数,一个是格式字符串、另一个是Object[]数组,这个数组保存着所有的参数(如果调用者提供的是整形数组或者其他基本类型,自动装箱功能将把他们转换成对象)。之后便将扫描fmt字符串并将第i个格式说明符与args[i]的值匹配起来。
即:对于printf的实现者来说,Object[]参数类型和Object...完全一样。
编译器需要对printf的每次调用进行转换,以便将参数绑定到数组上,并在必要的时候进行自动装箱。例如:
System.out.printf("%d %s",new Object[]{new Integer(n),"widgets"};)
用户也可以自定义可变参数的方法,并将参数指定为任意类型,甚至是基本数据类型。如:
public static double max(double... values){
double largest=Double.NEGATIVE_INFINITY;
for(double a:vaules){
if(a>largest)
largest=a;
}
return largest;
}
//可以进行如下调用:
double m=max(1,2,3,4,5,6,7);
//进行这个调用时,编译器将自动创建一个double数组new double[]{1,2,3,4,5,6,7},并将其传递给max方法
//m将得到7
【提示】
允许将一个数组传递给可变参数方法的最后一个参数。如:
System.out.printf("%d %s",new Object[]{new Integer(n),"widgets"};)
所以,可以将已经存在且最后一个参数是数组的方法重新定义为一个可变参数方法,而不会破坏任何已经存在的代码。
例如可以更改main方法的声明:
public static void main(String[] args) {
//main方法声明更改如下:
}
public static void main(String... args) {
//更改后,变为可变参数的main方法,可见并不会破环任何已存在的代码
}