Java基础篇3
五、Object通用的方法
equals
功能描述:判断两个对象是否等价。默认内部实现等价于 ==
对于等价的定义:
- 自反性(自身等价)
- 对称性(相互等价)
- 传递性(传递等价)
- 一致性(多次调用结果不变)
- 任何非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接口
浅拷贝:拷贝对象与原始对象的引用类型引用的是同一个对象。(牵一发动全身)
深拷贝:拷贝对象与原始对象的引用类型引用的是不同对象。(另起门户)
一般不使用该方法,而使用拷贝构造函数或者是拷贝工厂来拷贝一个对象。
六、多态与继承
多态(运行时多态)
定义:父类的某个方法被子类重写时,可以产生自己的功能行为。(同一消息可以根据发送对象的不同而采用不同的行为方式)
实现技术:动态绑定(在执行期间判断引用对象的实际类型,根据实际类型调用相应的方法,也可以称为实例调用)
重载则是编译时多态(运行时调用确定的方法)的案例。
实现条件
- 继承:必须存在继承关系的子类与父类;
- 重写:子类对父类中某些方法进行重新定义,则在调用这些方法时调用的是子类的方法;(顺序问题见重写部分的内容)
- 向上转型:需要将子类的引用赋给父类对象,才能调用父类和子类的方法。
多态的原理
详细部分见博客。
多态运行具体访问时实现方法的动态绑定。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关键字
访问父类构造函数:一般子类需要非默认的父类构造函数时,就会使用该关键字。
访问父类成员:子类重写了父类的某个方法,则使用该关键字来引用父类的方法实现。(将父类的实现作为子类实现的一部分)
重写与重载
重写
存在于继承的体系中,表示子类实现了一个与父类在方法声明上完全相同的方法。
在调用一个方法时,应先从本类方法中查找是否有对应的方法,没有再到父类查看。否则需要对参数转型,转成父类后看是否有对应的方法。
调用的优先级为:
- this.func(this)
- super.func(this)
- this.func(super)
- 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));
}
}
反射的实现方式
- 使用Class.forName(“className”)获取对应的Class对象;
- 利用Class对象可以获取 className类的构造器,属性与方法。
- 使用invoke在运行时动态调用某个实例的方法(传入的第一个参数是className类的实例)。
反射操作的缺点
性能开销: 涉及动态类型的解析,因此JVM无法对反射的代码进行优化。
安全限制:由此带来的安全问题。
内部暴露:反射代码可以访问私有属性与方法(可用于调试),破坏了抽象性,导致代码功能失调并破坏了可移植性。