继承
前面提到过,继承是基于已有的类去进行扩展建立新类的过程,不仅有父类的所有变量和方法,包括私人权限的(只不过自己也不可以调用而已,不过还是拥有的),可以扩展自己的变量和方法,也可以去重写父类的方法,可以更好地去设计类。
覆盖方法
super关键字的作用是指向父类,假设子类中出现与父类同名的的方法,在子类中调用是默认调用自己的(也可以用this关键字),那么如果想访问父类的公共属性或者公共方法,可以使用super关键字来访问父类的公共方法和公共变量(同理,父类的构造方法也是用super去调用,而且子类的构造方法的第一行必须是父类的super构造方法)。
多态
多态是指,引用的变量具体引用的实例是不确定的,只有在程序运行时候才确定。
比如:每个经理都是员工,但并不是每个员工都是经理
继承指出:子类的每个对象也是超类的对象,所以变量为父类,其引用可以为子类,这就是向下转型,即可以将子类的对象赋值给超类的变量,这个规则称为is-a规则,也就是这个规则,产生了多态
对于向下转型,有几条规则
-
可以调用的方法只有父类里面的方法
-
优先选用子类的方法(因为重写)
-
优先选用父类的变量
向下转型:父类引用子类
举个栗子
这是子类
/**
* @Author: Ember
* @Date: 2021/5/19 10:48
* @Description:
*/
public class SonTwo extends FatherTwo{
private Integer old;
private String name;
public Integer id;
public String nick;
public SonTwo(Integer old, String name, Integer id, String nick) {
super(2,"我是father","father",2);
this.old = old;
this.name = name;
this.id = id;
this.nick = nick;
}
public SonTwo() {
}
public Integer getOld() {
return old;
}
public void setOld(Integer old) {
this.old = old;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public Integer getId() {
return id;
}
@Override
public void setId(Integer id) {
this.id = id;
}
@Override
public String getNick() {
return nick;
}
@Override
public void setNick(String nick) {
this.nick = nick;
}
}
下面是父类
/**
* @Author: Ember
* @Date: 2021/5/19 10:50
* @Description:
*/
public class FatherTwo {
private Integer age;
private String name;
public String nick;
public Integer id;
public FatherTwo(Integer age, String name, String nick, Integer id) {
this.age = age;
this.name = name;
this.nick = nick;
this.id = id;
}
public FatherTwo() {
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNick() {
return nick;
}
public void setNick(String nick) {
this.nick = nick;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
}
调用方法测试一下方法与属性的优先级
public class TestExtend {
public static void main(String[] args) {
FatherTwo father = new SonTwo(1,"我是son",1,"son");
//方法是优先访问子类
System.out.println(father.getName());
System.out.println(father.getNick());
System.out.println(father.getId());
System.out.println(father.getAge());
//可以看到,会优先访问父类
System.out.println(father.id);
System.out.println(father.nick);
}
}
可以看到,没有子类的getOld方法,证明不可以调用父类没有的方法
阻止继承:final类和方法
给类加上final关键字,那么该类就不会被继承
给类中的特定方法加上final关键字,那么该方法就不可以被子类覆盖重写,final类中的所有方法自动地变成final方法
给属性加上final关键字,那么该属性必须在类构建实例的时候进行初始化,而且构建实例之后,这个属性不可以再被修改。
强制类型转化
前面已经学习过基本数据类型进行强制转换。
那么对于引用类型进行强制转化会怎样
将一个值赋值给变量时,编译器会去检查这样的行为是否承诺过多,如果将一个子类的引用赋值给父类的变量,那就没问题,如果将一个父类的变量引用赋值给子类,那么就承诺过多了(父类没有子类的完全功能,这个变量会缺少一部份功能),必须要进行强制转换。
应用类型的强制转化有如下规则
- 只能在继承层次内进行强制转换
- 在将父类强制转换成子类之前,应该使用instanceof进行检查
- instanceof会查看是否能够成功地转化,如果可以,那就返回True,如果不行,就会返回false
我们仍然使用上面的父与子类来测试
/**
* @Author: Ember
* @Date: 2021/5/19 10:51
* @Description:
*/
public class TestExtend {
public static void main(String[] args) {
FatherTwo father = new SonTwo(1,"我是son",1,"son");
FatherTwo fatherTwo = new FatherTwo();
System.out.println(father instanceof SonTwo);
System.out.println(fatherTwo instanceof SonTwo);
}
}
可以看到,若父引用要可以强转成子变量,前提是这个父引用是子引用来的。
抽象类
继承产生了一种自上而下的层次结构,最上面的类更加具有一般性,可能更加抽象,而下层的类则在不断扩展
祖先类更有一般性,人们只将它作为派生其他类的基类,而不会去用来实例化想要得到的对象
抽象类的规则如下
-
抽象类可以有抽象方法,其他属性和方法都跟一般类一致(可以有public、protected和default这些修饰符)
-
抽象类不可以去实例化
-
抽象类也可以有自己的静态变量和静态方法,同样也可以直接使用抽象类进行调用
-
继承抽象类的所有对象也会继承静态方法和静态变量
-
如果抽象类被继承(只能被单继承)
- 如果是抽象类继承抽象类,可以不去实现父抽象类的抽象方法(但该抽象方法也会被子抽象类继承下来)
- 如果是一般类继承抽象类,必须去实现父抽象类的抽象方法
- 继承类无论是抽象类还是子类,都要在构造方法上调用super来构造父抽象类的实例
-
抽象类也要有自己的构造方法,只可以被子类使用super来调用(因为属性还是位于抽象类里面,需要对外提供构造方法来进行实例化),在单元测试上是不可以调用的,也就是不支持使用new去实例
权限修饰符
- 对外部完全可见——public
- 对本包和所有子类可见——protected
- 对本包可见——默认,不需要修饰符
- 仅仅对本类可见——private
一般来说,想要进行限制某个方法或者变量的话,使用protected。
Object:所有类的超类
Object类是Java中所有类的始祖,每个类都扩展了Object,但是并不需要去手动继承
如果没有明确指出超类,Object就被认为是这个类的超类
可能这里有人以为,那明确指出超类,不就没有Object当父类了吗?
其实并不是这样,因为Object始终位于架构的最上层,也就是最模糊最抽象的一层。
Object的equals方法
可以看到Object实例的equals方法仅仅用于检测一个对象是否等于另外一个对象,即判断两个对象的引用地址是否相等
但Oject还提供了静态的比较方法
可以看到静态方法的equals不仅检测两个对象的引用地址是否相等,还会去检测一方是否为空,如果不为空,调用这一方的equals方法,前面提到过,如果Object a是由多态而来的,equals方法会优先调用子类的重写,所以这里不一定会调用object实例的equals
但一般这种机制不太够用,所以有时候类也要去重写equals方法。
/**
* @Author: Ember
* @Date: 2021/5/19 15:24
* @Description:
*/
public class Employee {
private String nickName;
private Integer employId;
public Employee(String nickName, Integer employId) {
this.nickName = nickName;
this.employId = employId;
}
public Employee() {
}
/**idea自动生成的重写方法**/
@Override
public boolean equals(Object o) {
//先判断是否是同一个引用
if (this == o) {return true;}
//判断比较的对象是否为空
//getClass方法是获取一个实例对象所属的类
//判断是不是通一个类
if (o == null || getClass() != o.getClass()){ return false;}
//强转成同一个类
Employee employee = (Employee) o;
//比较两个类的属性是否一样
//这里使用equals进行比较,注意这里的是静态方法equals
//也就是,如果属性如果为引用类型,那么就会优先调用该引用类型重写的equals方法
return Objects.equals(nickName, employee.nickName) &&
Objects.equals(employId, employee.employId);
}
@Override
public int hashCode() {
return Objects.hash(nickName, employId);
}
}
可以看到,除了比较应用地址和类的类型之外,还会进行比较值,比较值的过程如下
- 调用Object的静态equals方法
- 会先比较属性的引用地址
- 然后比较其中一方是否为null,如果为null,就返回false
- 如果其中一方不为null,就会调用这一方的equals方法进行比较
- 继承关系会优先调用子类的equals方法
相等测试与继承
对于equals方法,可以看到只要class不一样,就会返回false,那么可能有一些人就想子类也能与父类进行比较
那么可以将类判断改成如下
//假设Employee是一个超类,Manager是一个子类
if(!(otherObject instanceof Employee)){return false;}
那么Employ实例的equals方法也可以跟Manager进行属性比较了
但强烈建议这种方式不太好
Java语言规范要求equals方法要有下面的特性
- 自反性:对于任何非空引用x,x.equals(x)应该返回true
- 对称性:对于任何引用x和y,x.equals(y)返回True时,那么y.equals(x)也要返回True
- 传递性:对于任何引用x、y和z,x.equals(z)返回True,y.equals(z)返回True,那么x.equals(y)也要返回True
假如使用了上面的判断
employee.equal(manager) //这是可以的,因为Manager instance Employee为true
manager.equal(employee) //这是不可以的,因为Employee instance Manager为false(承诺过多)
这样不就违反了对称性了嘛?
hashcode方法
hashcode是散列码,由对象导出的一个整数值,散列码是没有规律的,只要是不同对象,基本上散列码是不会出现相同的。
hashcode是怎么生成的
可以看到hashCode()是一个native方法,也就是由底层的C++或C去生成的,其实计算方式是根据对象在堆的存储地址去进行散列得出的
这个散列码的作用是用来确定该对象在哈希表的索引位置,只有在散列表里才有用,在其他情况下是没用的
假设建了一个对象集合,比如HashSet,Set集合是不允许重复的,那么怎么去判断这两个对象相同呢?使用遍历吗?使用遍历不就是最低效的一种吗?所以就有了hashcode
对象在哈希表中的位置,是通过hashcode生成的(具体生成方式以后再说),那么就可以通过hashcode来判断位置是否有对象,来区别出是否出现重复现象,如果有对象,那么再调用该对象的equals方法去判断究竟是不是一样(因为hashcode一样,不代表对象一样,具体还是要使用equals方法去判断)。
关于hashcode与equals方法
如果程序中不会创建类对应的散列表,那么hashcode方法与equals方法没有一点关系
因为即使hashcode一样,对象也不一定相等;对象相等,hashcode也不一定相等(不重写hashcode的情况下)
如果程序中需要创建类对应的散列表,那么hashcode是有关系的
- 如果两个对象相等,那么他们的hashcode一定要相等(所以要重写hashcode,不重写的话无法进行去重,因为hashcode不同会被分配在散列表不同位置,不会产生冲突)
- 如果hashcode相等,那么这两个对象不一定要相等
- 目的就是为了可以减少散列表判断重复的时间复杂度
接下来看看为什么相同的对象,hashcode一定相同吧,这里从IDE自动生成的hashcode来说明
可以看到,其调用了Objects.hash静态方法,然后参数是类里面的所有属性
可以看到这个方法的参数是一个任意数量的Object类型,调用Arrays的hashCode静态方法
//任意数量的参数,其实可以隐式转变为数组
public static int hashCode(Object a[]) {
//如果为空,那么hashCode就返回0
if (a == null)
return 0;
//默认为1
int result = 1;
//具体计算方式为如下
//可以看到是根据类的所有属性去进行计算的
for (Object element : a)
//计算方式为31倍result加上属性的hashCode
//如果是基本数据类型,那么会自动装箱,调用引用类型的hashCode
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}