重拾Java之路-面向对象特性

五、Object通用的方法

equals

功能描述:判断两个对象是否等价。默认内部实现等价于 ==

对于等价的定义:

  1. 自反性(自身等价)
  2. 对称性(相互等价)
  3. 传递性(传递等价)
  4. 一致性(多次调用结果不变)
  5. 任何非null对象与null比较的结果都是false
等价于相等的区别

对于基本类型, 直接使用 == 来判断是否相等,无equals方法。
对于类库中的引用类型, == 用于判断两个变量是否引用同一个对象(只看栈中值),而equals则会判断两个对象是否等价(不仅看值,还看地址)。这里的equals方法已经被重写。

public class EqualsMethod{
    private int x;
    private int y;
    private int z;


    public EqualsMethod(int x, int y, int z){
        this.x = x;
        this.y = y;
        this.z = z;
    }

    // 经过覆盖的方法只判断内部的关键域
    @Override
    public boolean equals(Object o){
        if(this == o) return true;
        if(o == null || getClass() != o.getClass())
            return false;

        EqualsMethod that  = (EqualsMethod) o;

        if(x != that.x) return false;
        if(y != that.y) return false;

        return z == that.z;
    }
}

hashcode

功能描述:返回对象的散列值。注意:等价的对象散列值一定相同,反之则不一定成立。

HashSet与HashMap使用了该方法用于计算对象应该存储的位置,因此,如果要让自定义等价对象正常使用这些集合(计算出相同Hash值),则需要实现该方法。

EqualsMethod e1 = new EqualsMethod(1, 1, 1);
EqualsMethod e2 = new EqualsMethod(1, 1, 1);
System.out.println(e1.equals(e2)); // true 两者等价
HashSet<EqualsMethod> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2 但是hash值不同,导致集合不认为它们等价

toString

功能描述:默认返回的是诸如 Basement.object.EqualsMethod@50cbc42f 的形式, @后面为散列码的无符号十六进制表示形式。

该方法一般需要覆盖,然后输出自己想要的对象的信息。

clone

功能描述:它是protected方法,如果不显式重写,其它类就不能调用该类实例的clone() 方法。

注意:如果一个类没有实现Cloneable接口而又调用了clone方法,则会抛出 CloneNotSupportedException 异常。
因此,如果需要重写该方法,类必须实现Cloneable接口

浅拷贝:拷贝对象与原始对象的引用类型引用的是同一个对象。(牵一发动全身)
深拷贝:拷贝对象与原始对象的引用类型引用的是不同对象。(另起门户)

一般不使用该方法,而使用拷贝构造函数或者是拷贝工厂来拷贝一个对象。

六、多态与继承

多态(运行时多态)

定义:父类的某个方法被子类重写时,可以产生自己的功能行为。(同一消息可以根据发送对象的不同而采用不同的行为方式

实现技术:动态绑定(在执行期间判断引用对象的实际类型,根据实际类型调用相应的方法,也可以称为实例调用

重载则是编译时多态(运行时调用确定的方法)的案例。

实现条件

  1. 继承:必须存在继承关系的子类与父类;
  2. 重写:子类对父类中某些方法进行重新定义,则在调用这些方法时调用的是子类的方法;(顺序问题见重写部分的内容)
  3. 向上转型:需要将子类的引用赋给父类对象,才能调用父类和子类的方法。

多态的原理

详细部分见博客

多态运行具体访问时实现方法的动态绑定。Java的实现主要依赖于 方法表 ,通过继承和接口的多态实现有所不同。

PS:所有继承父类的子类方法表中,父类定义的方法偏移量是一个定值。

继承:通过方法表查询,获取方法在方法表中的偏移量,如果方法已经重写,则直接调用,否则进行递归查找调用。

接口:需要通过搜索完整的方法表,不能通过偏移量的方法来查询。从效率上看,接口的方法调用是慢于类方法调用的。

访问权限

一共有四种,分别是 private、protected、public 以及不加访问符,最后一种表示包级可见。(同一个包内是可以访问的)

对于非内部类而言,类访问修饰符只有不加与public两种。

protected用于修饰成员,其可见性在于两点:
基类的protected成员是包内可见的,并且对子类也可见。
若子类与基类不在一个包中,那么在子类中,子类的实例可以访问从基类继承的protected方法,而不能访问基类实例的protected方法。(看最终来源)

抽象类与接口

抽象类不能被实例化,只能被继承。

public abstract class AbstractClassExample {
    protected int x;
    private int y;

    public abstract  void func1();

    public void func2(){
        System.out.println("func2");
    }
}

public class AbstractExtendClassExample extends AbstractClassExample{
    // 必须实现抽象类未实现的方法
    @Override
    public void func1() {
        System.out.println("func1 test");
    }

    public static void main(String[] args) {
        AbstractClassExample ac2 = new AbstractExtendClassExample();
        ac2.func1();
        ac2.func2();
    }
}

接口是抽象类的延伸,从Java8开始,接口可以有默认的方法实现。从Java9开始,接口允许定义为 private 与 public 两种。

接口的字段默认是static与final的。(与类共存亡+常量值)

public interface InterfaceExample {
    void func1();

    default void func2(){
        System.out.println("func2 test");
    }

    // 字段必须初始化
    int x = 123;
    // 接口的字段与方法默认是 public 的,不允许定义为 private或 protected
//     private int k = 0;
}

public class InterfaceExampleImplement implements InterfaceExample{
    public void func1(){
        System.out.println("func1 implement");
    }

    public static void main(String[] args) {
        InterfaceExample ie2 = new InterfaceExampleImplement();
        ie2.func1();
        ie2.func2();
    }
}
关于两者的比较

设计层面:抽象类提供了一种IS-A的关系,子类对象必须能够替换所有的父类对象。接口则更接近LIKE-A的关系,提供一种方法的实现契约,是抽象方法的集合(还有常量)。

使用:一个类可以实现多个接口,但是不能继承多个抽象类。

接口的字段是static和final类型的,成员是public的(Java9 以后多了 private);抽象类没有这类要求。

super关键字

访问父类构造函数:一般子类需要非默认的父类构造函数时,就会使用该关键字。
访问父类成员:子类重写了父类的某个方法,则使用该关键字来引用父类的方法实现。(将父类的实现作为子类实现的一部分)

重写与重载

重写

存在于继承的体系中,表示子类实现了一个与父类在方法声明上完全相同的方法。

在调用一个方法时,应先从本类方法中查找是否有对应的方法,没有再到父类查看。否则需要对参数转型,转成父类后看是否有对应的方法。

调用的优先级为:

  1. this.func(this)
  2. super.func(this)
  3. this.func(super)
  4. super.func(super)

重载

存在于同一个类中,方法名称相同,但是参数类型,个数,顺序至少有一个不同。
PS:返回值不同,其它相同不是重载。

七、反射

反射就是在运行时才知道操作的类的是什么,并且可以在运行时获取类的完整构造并且调用对应的方法。

public class Reflection {
    private int price;

    public int getPrice(){
        return this.price;
    }

    public void setPrice(int price){
        this.price = price;
    }

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // general method
        Reflection test = new Reflection();
        test.setPrice(100);
        System.out.println(test.getPrice());

        // reflection
        // 先获取类对象实例
        Class clz = Class.forName("basement.foundation.Reflection");
        // 获取构造器对象实例
        Constructor constructor = clz.getConstructor();
        Object testObject = constructor.newInstance();

        // 获取方法对象实例并使用invoke来调用
        Method setPriceMethod = clz.getMethod("setPrice", int.class)
        setPriceMethod.invoke(testObject, 100);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println(getPriceMethod.invoke(testObject));
    }
}

反射的实现方式

  1. 使用Class.forName(“className”)获取对应的Class对象;
  2. 利用Class对象可以获取 className类的构造器,属性与方法。
  3. 使用invoke在运行时动态调用某个实例的方法(传入的第一个参数是className类的实例)。

反射操作的缺点

性能开销: 涉及动态类型的解析,因此JVM无法对反射的代码进行优化。
安全限制:由此带来的安全问题。
内部暴露:反射代码可以访问私有属性与方法(可用于调试),破坏了抽象性,导致代码功能失调并破坏了可移植性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值