public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native Class<?> getClass()
protected void finalize() throws Throwable {}
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
在JVM体系结构中,有一个Java Native Interface模块,称为Java本地库接口,它是用来融合Java与其它编程语言的,同样的还有一个融合不同语言的运行环境是CLR,这里就不多加赘述了。我们都知道Java是跨平台的这么一种语言,通过JVM的方式,这种方式导致了Java控制不了一些底层的东西,为了修复这个缺口,就要向其它语言请求帮助,这就是native的目的了。
声明了native关键字后,这个方法就像接口里的方法(JNI,说的就是给其它语言接口)在声明时没有方法体,它的方法体就交给了具体实现这个方法的那个语言去实现。在调用java方法时,JVM会向它的栈内压入一个栈帧,在调用native方法时,实际调用的是其他语言的方法,这时候只是简单的动态连接到本地方法栈的相应方法。
native修饰的东西,是在其它语言中实现的。
个人认为,equals()是个仁者见仁智者见智的东西,因为它可以被重写,没有人管得着你想怎么写你的equals()。但是,就像要有个传统一样的东西,equals表达的疑似是两个对象是否等价,啥是等价?这是一个数学概念:
自反性:
x.equals(x); // true
很简单,我等价我自己。
对称性:
x.equals(y) == y.equals(x); // true
我和你等价,你和我等价。
传递性:
if (x.equals(y) && y.equals(z))
x.equals(z); // true;
我和你等价,你和他等价,那我和他等价。
同时还有另外特性,在计算机科学中需要考虑:
一致性:
x.equals(y) == x.equals(y); // true
我和你等价这种事不能变。
与NULL比较为false:
x.equals(null); // false;
我和null不等价。
hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。这里设计到一个问题,两个值不同的对象可能计算出相同的哈希值这一问题。如果我们不重写hashCode,它返回的是该对象在jvm的堆上的内存地址,不同对象,内存地址肯定不同。如果重载,我们可以看到,hashCode()返回的是一个int型的数据,也就是说最多存在2^32次方个散列值,那么出现重复的散列值就不奇怪了。
同时,hashCode有个常规协定,我做个总结就不放出来了:
可以看到总结的第二条当中equals方法和hashCode有个交互,所以为了维护这个常规协定,我们需要同时重写这两个方法。
HashSet和HashMap都要调用对象的hashCode方法来计算出对象的存储位置。其中,HashMap需要注意的一点是,它是计算key的hashCode来获取存储位置。那么这里就可以有一个有意思的实验了,实验配方:
重点:HashSet的add方法,实际上是调用了HashMap的put方法(HashSet是借助HashMap实现的),而put方法的执行差不多是这样的:先调用对象的hashCode如果map里面没有这个hash值,那么就直接添加这个新的对象到map;如果有这个hash值,就判断这个对象和新对象是否等价,也就是调用equals,如果返回true则表明我Map里面已经有了,不插入,如果返回false则表明我map里面还没这个对象,插入。
理想的哈希函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了哈希函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。
R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位,最左边的位丢失。并且一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x
,编译器会自动进行这个优化。
@Override
public int hashCode() {
int result = 17;
result = 31 * result + x;
result = 31 * result + y;
result = 31 * result + z;
return result;
}
这个地方告诉我们,在使用自己的类的时候,如果涉及HashMap和HashSet,就要格外注意hashCode和equals的重写。
toString 默认返回的格式:完整类名@散列码的无符号十六进制。
举个例子:
public static void main(String[] args) {
JavaBlogTest x = new JavaBlogTest();
System.out.println(x.toString());// a.JavaBlogTest@1
}
下面是JavaBlogTest类:
package a;
public class JavaBlogTest {
@Override
public int hashCode() {
return 1;
}
}
clone() 是 Object 的 protected 方法,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。对于Object,它的clone只在java.lang包和它的子类内可见。protect的规则不是三言两语能说清的,这里提供一个传送门(菜鸟教程.)。在重写clone方法时要同时implements Cloneable,这个接口规定了如果一个类没有实现Cloneable就调用了clone()方法,会抛出CloneNotSupportedException。
对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝,Object中的clone方法就是浅拷贝。
创建一个Basic类,我们等会要使用它,用来验证引用数据类型。
public class Basic {
private int c;
public int getC() {
return c;
}
public void setC(int c) {
this.c = c;
}
}
创建一个使用Basic的类,里面还包括基本数据类型Integer(我删掉了一部分getter和setter为了容易看):
public class JavaBlogTest implements Cloneable {
private int[] arr;
private Integer a;
private Basic c;
public Integer getC() {
return c.getC();
}
public void setC(Integer c) {
this.c.setC(c);
}
public JavaBlogTest(){
a = 1;
b = 1;
c = new Basic();
c.setC(1);
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
@Override
protected JavaBlogTest clone() throws CloneNotSupportedException {
return (JavaBlogTest)super.clone();
}
public static void main(String[] args) {
JavaBlogTest a = new JavaBlogTest();
JavaBlogTest b = null;
try{
b=a.clone();
} catch (CloneNotSupportedException e){
e.printStackTrace();
}
a.set(2, 222);
System.out.println(b.get(2));// 222
a.setA(2);
System.out.println(b.getA());// 1
a.setC(2);
System.out.println(b.getC());// 2
}
}
我把相应结果放在注释了,可以看到对数组和自创的类(也就是引用类型)在原始对象进行修改,拷贝对象的相应也变了,也就我们在c++所说的按地址传递。对于基本数据类型,这里的Integer,在原始对象进行修改,拷贝对象是不变的,也就是我们在c++所说的按值传递。
对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。可以看到,深拷贝和浅拷贝的不同点在于引用数据类型。对于引用数据类型,在浅拷贝中只按引用传递,就导致牵一发而动全身,而有的时候我们更希望复制出来的是一个完完全全新的对象,它们的引用类型的变量并不指向同一个对象,而是不同的对象,只是新对象和老对象的关键值是相等的,这种处理方式就叫深拷贝。
我修改了一下前面浅拷贝中的clone函数,让它深拷贝:
@Override
protected JavaBlogTest clone() throws CloneNotSupportedException {
JavaBlogTest result = (JavaBlogTest)super.clone();
result.c = new Basic();
result.c.setC(this.c.getC());// 同步关键值,我需要这个值被复制
result.arr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {// 同步关键值,我需要这个值被复制
result.arr[i] = arr[i];
}
return result;
}
对于引用类型的具体内容,你可以选择留下(也就是我所谓的同步关键值),也可以选择不要,但留下才代表是真正的深拷贝。
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
clone默认浅拷贝,也就是引用类型数据是指向同一个对象的,深拷贝需要自己实现,而且建议使用拷贝构造函数和拷贝工厂来实现拷贝。
下面的表格是摘自菜鸟教程。
修饰符 | 当前类 | 同一包内 | 子孙类(同一包) | 子孙类(不同包) | 其他包 |
---|---|---|---|---|---|
public | Y | Y | Y | Y | Y |
protected | Y | Y | Y | Y/N | N |
default | Y | Y | Y | N | N |
private | Y | N | N | N | N |
抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。
使用接口:
使用抽象类:
重写,意味着子类实现了一个和父类在声明上相同的方法,这个相同不包括返回类型,访问权限和抛出异常类型的完全相同,也就是有如下三个原则:
在同一个类中,方法名相同,但参数类型、顺序、个数不完全相同,返回值不同不算。
每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。
有三种获得Class对象的方式:
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
这里提供一个博客,很详细的实验,以及反射的相关方法:简书
反射,给类的使用提供了很大的便利,使得私有的方法变量都可以直接访问了,但因此缺点也很明显,不安全,而且性能开销大,因为反射涉及了动态类型解析。