四、Object超类
Object:Java中所有类的始祖,也就是祖宗(根父类),每个类都是由它扩展而来的,包括数组的父类也是Object。如果在类中声明未使用extends关键字指明父类,就默认父类为java.lang.Object类。
Objcet类结构预览图:所有类都具有了其Object的方法,Object无属性,其并不是抽象类,可以进行实例化,只有一个空参构造器,其中部分方法是final类型,子类无法重写。
问1:Object不是抽象类,其是具体的,为什么会允许有人去创建Object对象?是不是不合理?
因为有时候就会会需要一个通用的对象,一个轻量化的对象。最常见的用途是用来线程的同步化上。
问2:Object主要目的是提供多态的参数与返回类型吗?
①作为多态让方法可以应付多种类型的机制。
②提供Java在执行期对任何对象都有需要的方法。
③有一部分与线程相关。
问3:既然多态这么有用,为什么不把所有参数与返回类型都设置为Object?
java对于保护程序代码有一项重要机制"类型安全检查",使用多态时再编译期间只会将左边声明类作为实例,这也代表你只能调用该类的方法。例如:Object o = new Person(); o.walk(); 那么第二条调用方法无法通过编译,因为walk()在Object中并没有,编译期间只会认定Object作为实例。
Java是类型检查很强的语言,编译器会检查你调用的是否是该对象确实可以响应的方法。
1、==运算符与equals方法
==运算符
==运算符:可使用于基本数据类型变量与引用数据类型变量
基本数据类型变量:比较两个保存的字面值是否相等(不一定类型相同,如整型、浮点型)
引用数据类型变量:比较两个对象的地址值是否相等。实际上就是两个引用地址是否是在堆中同一个开辟的对象(即在堆中的地址)。
比较特殊举例:
//基本数据类型:整型与浮点型比较 int a = 1; int b = 1.0; System.out.println(a == b);//true //引用类型:创建对象的String字符串 堆中开辟的内存地址不同,所以为false String str = new String("changlu"); String str1 = new String("changlu"); System.out.println(str == str1);//fasle //引用类型:直接赋值的String字符串 直接赋值的是存于方法区中的常量池里,相同的字符串会引用同样的地址,不会再重新开辟一份空间 String str2 = "changlu"; String str3 = "changlu"; System.out.println(str2 == str3);//true
equals方法
equals:仅适用于引用数据类型
每个类都会有默认的equals方法(因为都继承了Object类),看一下Object类中equals方法:
public boolean equals(Object obj) { return (this == obj); }
在Object中的equals方法与==的作用相同,比较两个引用地址值是否相同。
而实际上我们对于对象的比较应该是对其中类的属性进行比较,所以一般都会重写equals方法,在String,Date,File,包装类中实际上重写了Object类中的equals方法,这些实际上都是比较其实体内容是否相等。
看一下String类中重写的equals方法:
public boolean equals(Object anObject) { //首先看是否是一个地址的引用 if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
2、toString()方法
toString():用来描述我们对象的信息
首先看一下Object中的toString()方法:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
其中的内存对象地址是虚拟地址,在操作系统之上盖了一层JVM,JVM相当于虚拟的操作系统,所以说自定义类(没重写equals方法的)输出的是虚拟的地址,而不是真实的内存地址。
我们之前使用System.out.println()输出一个自定义对象时,输出的是一个如Boy@677327b6,实际上就与这个Object的toString()有关,看一下源码:
//System源码:System.out =》PrintStream out = null; public void println(Object x) { //调用了String的valueOf() String s = String.valueOf(x); synchronized (this) { print(s); newLine(); } } //String源码:valueOf()方法 public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString(); }
我们可以看到通过println()输出的实际上就是自定义对象toString()方法,valueOf()方法中采用了多态,由于我们自定义类没有重写toString()方法,所以会默认调用Object中的进行输出也就是一个虚拟地址值了。
toString()自定义如下:
class Person{ public String name = "person"; //这是IDEA自动生成的toString,我们也可以修改成自己想要输出的形式 @Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; } } public class Main { public static void main(String[] args){ System.out.println(new Person());//Person{name='person'} } }
通过自定义toString()方法,我们再次输出就不再是一个虚拟地址值了(并不是说明栈中引用变量不再是堆中的内存地址了),可以输出类中自定义内容。
五、包装类
介绍拆装箱与转换问题
包装类:针对于八种基本类型定义相应的引用类型,也称为封装类。
基本数据类型 |
包装类 |
byte |
Byte |
boolean |
Boolean |
short |
Short |
char |
Character |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
JDK5.0之后引入了装箱与拆箱的概念。
装箱操作:Integer i = 15; 隐藏了Integer.valueOf(15)方法,实际上也是通过new 对象获得的对象实例。
拆箱操作:int i1 = i; 隐藏了Integer.intValue()方法,返回int值。
针对于包装类如何创建实例?可直接赋值或者通过有参构造器。
一般直接赋值,如Integer i = 1;,或者通过有参构造器 Integer i = new Integer(100);
比较特殊的是Boolean类提一下,其通过构造器创建实例如Boolean boolean = new Boolean("tRue");,其中true与false可以不区分大小写,其他有误。
基本数据类型与包装类转换:
包装类 =》基本数据类型:xxxValue()
基本数据类型 =》包装类:valueOf(xxx)
基本数据类型、包装类与String类转换:
基本、包装 =》String类:String.valueOf(xxx)
String类 =》基本数据类型:对应包装类.parsexxx()
Integer包装类容易混淆情况
在jdk1.5之后,Integer与int之间能够进行自动拆箱与装箱。
Integer中的缓存数组问题
先看这个例子:
@Test public void test02(){ Integer i = 10; Integer j = 10; System.out.println(i == j);//true }
两个Integer包装类实例直接赋值10,中间会有装箱的过程会自动调用Integer.valueOf()方法,此时就会有一个疑问抛出,为什么我们声明的是两个引用类型,最后==判断引用地址问什么相同?
针对于该问题我们应当去Integer源码中一探究竟:
//包装类源码 public final class Integer extends Number implements Comparable<Integer> { //装箱过程会调用valueOf()方法,其中会对i进行判断是否在[-128,127]中,若是的话直接返回一个cache[xx]指定下标值, public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) //若是10,则返回cache[138]=>10 return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } //静态内部类IntegerCache:用于预先存储[-128,127]的所有值到Integer数组cache中,数组中都是一个个Integer实例 private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; //cache[0]赋值-128向上累加 for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); assert IntegerCache.high >= 127; } private IntegerCache() {} } }
总结:在进行装箱过程中使用的valueOf(int i)方法,其中i若是在[-128-127]区间中,那么返回就是就是cache数组中已经保存好的Integer实例(初始化类时保存),所以上面的程序结果为true,都是在cache数组中同一个实例。
new Integer()案例
@Test public void test02(){ Integer i = new Integer(10); Integer j = new Integer(10); System.out.println(i == j);//false }
注意:这里是new的实例,在堆中是各自独立开辟出来的对象实例,所以结果为false,并没有调用valueOf()这个方法。
看下源码:
public Integer(int value) { this.value = value; }
在Integer包装类中使用final int value来存储数据的。
相关面试题
1.三元运算符:若是包含三元运算符,会进行自动类型提升,例如下面的Integer会提升为Double,最后输出1.0。
Object o = true?new Integer(1):new Double(2.0); System.out.println(o);//1.0
2.if-else判断对于自动类型不会提升。
Object o; if(true){ o = new Integer(12); }else{ o = new Double(12.0); } System.out.println(o);//12
参考文章
[1]. Java创建子类实例的时候也会创建父类实例吗?
[2]. 子类将继承父类所有的属性和方法吗?为什么? 看其回答
[3]. [java]为什么System.in/out/err值为null?
[4]. 书籍《head first java 2.0》