Java知识复习(后篇)

Java复习(二)

  • Obeject方法
    • 方法总览
    • native关键字
      • 是啥?去哪儿?
      • 具体一点
      • 来个综上所述
  • equals()
    • 基本概要
    • 等价和==
  • hashCode()
    • 基本概要
    • HashSet和HashMap与hashCode的缘分
    • 一个理想的hashCode
      • 小总结
  • toString()
  • clone()
    • 基本概要
    • 浅拷贝
      • 举个大例子
    • 深拷贝
    • clone()的替代方案
    • 小结一下
  • 继承
    • 访问权限
    • 接口和抽象类
      • 抽象类
      • 接口
      • 比较
      • 用处
    • 重写||重载
      • 重写
      • 重载
  • 反射
    • Class对象
    • 反射定义
    • reflect相关类
    • 实验
    • 优缺点

Obeject方法

方法总览

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

native关键字

是啥?去哪儿?

在JVM体系结构中,有一个Java Native Interface模块,称为Java本地库接口,它是用来融合Java与其它编程语言的,同样的还有一个融合不同语言的运行环境是CLR,这里就不多加赘述了。我们都知道Java是跨平台的这么一种语言,通过JVM的方式,这种方式导致了Java控制不了一些底层的东西,为了修复这个缺口,就要向其它语言请求帮助,这就是native的目的了。

具体一点

声明了native关键字后,这个方法就像接口里的方法(JNI,说的就是给其它语言接口)在声明时没有方法体,它的方法体就交给了具体实现这个方法的那个语言去实现。在调用java方法时,JVM会向它的栈内压入一个栈帧,在调用native方法时,实际调用的是其他语言的方法,这时候只是简单的动态连接到本地方法栈的相应方法。

来个综上所述

native修饰的东西,是在其它语言中实现的。

equals()

基本概要

个人认为,equals()是个仁者见仁智者见智的东西,因为它可以被重写,没有人管得着你想怎么写你的equals()。但是,就像要有个传统一样的东西,equals表达的疑似是两个对象是否等价,啥是等价?这是一个数学概念:

  1. 自反性:

    	x.equals(x); // true
    

    很简单,我等价我自己。

  2. 对称性:

    	x.equals(y) == y.equals(x); // true
    

    我和你等价,你和我等价。

  3. 传递性:

    	if (x.equals(y) && y.equals(z))
        x.equals(z); // true;
    

    我和你等价,你和他等价,那我和他等价。

同时还有另外特性,在计算机科学中需要考虑:

  1. 一致性:

    	x.equals(y) == x.equals(y); // true
    

    我和你等价这种事不能变。

  2. 与NULL比较为false:

    	x.equals(null); // false;
    

    我和null不等价。

等价和==

  1. 对于基本类型,==能判断两个值是否相等,基本类型没有equals方法(基本类型和Object没有关系)
  2. 对于引用类型,"==" 判断两个变量是否引用同一个对象(也就是 "==“两边如果是对象的引用,只是判断这个引用也就是地址是不是相等),equals判断引用的对象是否等价(你要先实现,如果不实现,按源码的意思还是判断两个引用是不是一个东西和”=="一样)。

hashCode()

基本概要

hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。这里设计到一个问题,两个值不同的对象可能计算出相同的哈希值这一问题。如果我们不重写hashCode,它返回的是该对象在jvm的堆上的内存地址,不同对象,内存地址肯定不同。如果重载,我们可以看到,hashCode()返回的是一个int型的数据,也就是说最多存在2^32次方个散列值,那么出现重复的散列值就不奇怪了。

同时,hashCode有个常规协定,我做个总结就不放出来了:

  1. 一致性,多次调用必须一样。
  2. equals方法放回true,hashCode也必须一样

可以看到总结的第二条当中equals方法和hashCode有个交互,所以为了维护这个常规协定,我们需要同时重写这两个方法。

HashSet和HashMap与hashCode的缘分

HashSet和HashMap都要调用对象的hashCode方法来计算出对象的存储位置。其中,HashMap需要注意的一点是,它是计算key的hashCode来获取存储位置。那么这里就可以有一个有意思的实验了,实验配方:

  1. 生一个类叫A,重写它的hashCode()和equals()方法,让hashCode返回一个不变值,让equals返回true。
  2. 让A搞对象,搞两个,把它们使用add方法往HashSet里加。
  3. 检查一下HashSet的size。

重点:HashSet的add方法,实际上是调用了HashMap的put方法(HashSet是借助HashMap实现的),而put方法的执行差不多是这样的:先调用对象的hashCode如果map里面没有这个hash值,那么就直接添加这个新的对象到map;如果有这个hash值,就判断这个对象和新对象是否等价,也就是调用equals,如果返回true则表明我Map里面已经有了,不插入,如果返回false则表明我map里面还没这个对象,插入。

一个理想的hashCode

理想的哈希函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了哈希函数要把所有域的值都考虑进来。可以将每个域都当成 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()

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()

基本概要

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()的替代方案

使用 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
  1. public 到处可见。
  2. 比较复杂,你可以去这里(菜鸟教程.)深造一下。我的理解(用于判断当前调用是否合理的方法)是“一圈一线”,“一圈”:代表包内可见,先判断这个方法所属的包(需要一直回溯到根源,比如clone就是java.lang包的,继承下来的不算);“一线”:代表子孙类内可见,爷爷-父亲-儿子,爷爷可以叫爸爸儿子做事,但父亲和儿子叫不了爷爷做事情。也就是爷爷可以调用爸爸儿子的方法,儿子调用不了爸爸爷爷的。关于方法调用,有两种情况,一种是外部(不在继承树的所有类内)调用一棵继承树内方法,一种是继承树内互相调用。当外部调用的时候就要判断包,是不是同一个包,是就可以,不是就不行。当内部调用的时候,就判断当前被调用方法的对象是属于当前调用环境的子孙还是祖宗,如果是子孙就行,祖宗就不行。(这里的继承树我更喜欢用方法的继承树,比如A继承自Object且没有重写clone,那么A的clone方法它的祖宗是Object的clone方法,如果B重写了clone方法,那么B的clone方法就是所有B子代clone方法的祖宗,而不是Obeject的clone方法)
  3. default包内可见
  4. private类内可见

接口和抽象类

抽象类

抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。

接口

  1. 在Java1.8支持方法默认实现,也就是接口内方法允许拥有方法体,需要default关键字修饰。
  2. 接口的字段默认都是 static 和 final 和 public 的。

比较

  • 从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
  • 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
  • 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
  • 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。

用处

使用接口:

  • 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
  • 需要使用多重继承。

使用抽象类:

  • 需要在几个相关的类中共享代码。
  • 需要能控制继承来的成员的访问权限,而不是都为 public。
  • 需要继承非静态和非常量字段。
    在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。

重写||重载

重写

重写,意味着子类实现了一个和父类在声明上相同的方法,这个相同不包括返回类型,访问权限和抛出异常类型的完全相同,也就是有如下三个原则:

  1. 子类访问权限要比父类的访问权限大
  2. 子类的返回类型要是父类返回类型的子类型或就是父类的返回类型。
  3. 子类抛出的异常类型也必须是父类抛出的异常类型的子类或其本身。
    我们可以使用@Override注解,编译器会自动检查上面三个条件。
    由于重写的存在,就有了父子类方法的调用顺序,顺序如下:
  • this.func(this)
  • super.func(this)
  • this.func(super)
  • super.func(super)
    其实就是,如果方法不存在,就向上转型,先转前面再转参数,都不行就全转。

重载

在同一个类中,方法名相同,但参数类型、顺序、个数不完全相同,返回值不同不算。

反射

Class对象

每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。
有三种获得Class对象的方式:

  • Class.forName(“类的全限定名”)
  • 实例对象.getClass()
  • 类名.class (类字面常量)

反射定义

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

reflect相关类

  1. Field:代表类的成员变量
  2. Method:代表类的方法
  3. Constructor:代表类的构造方法

实验

这里提供一个博客,很详细的实验,以及反射的相关方法:简书

优缺点

反射,给类的使用提供了很大的便利,使得私有的方法变量都可以直接访问了,但因此缺点也很明显,不安全,而且性能开销大,因为反射涉及了动态类型解析。

你可能感兴趣的:(java,反射,抽象类,object)