什么是Java继承?
继承,顾名思义,比如儿子继承父亲的外貌与性格,徒弟继承师傅的手艺等,在Java编程中,继承同样如此,子类继承父类的属性和方法,用于描述两个类之间的关系。
那继承有什么用,为什么会用到它呢?
我们发现在描述过程中,它们有一些相同的属性和方法,也有一些不同的属性和方法。最好能有一种方法能把这些重复代码收集起来,然后每次要使用的时候,就直接调用这个方法,进行重复利用就可以了,而这种方法就是继承了。
将一些具有相似逻辑的类中的公共的属性和方法抽取出来,组成一个类,这个类我们称之为父类。父类和子类是一种一般和特殊的关系。
继承特点:
- 继承利于代码复用,能够缩短开发周期
- 继承用来描述一种类与类之间的关系
- 继承使用已存在的类的定义作为基础建立新类
- 新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类的功能
继承的实现
在Java中使用extends关键字实现类与类之间的继承关系,以动物Animal和狗Dog为例:
父类:
父类又被称为超类(Super Class),父类有其私有以及公有的属性和方法
public class Animal {
private String name;//名称
private int month;//月份
private String species;//品种
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
public Animal() {
}
public Animal(String name, int Month, String species) {
super();
this.setName(name);
this.setMonth(Month);
this.setSpecies(species);
}
}
子类:
子类又被称为派生类
public class Dog extends Animal {
//自有的属性
private String sex;
//自有的睡觉方法
public void sleep() {
System.out.println(this.getName() + "现在" + this.getMonth() + "个月大,它在睡觉~");
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Dog() {
}
public Dog(String name, int Month, String species, String sex) {
//利用super()可以调用父类的构造器
super(name, Month, species);
this.sex = sex;
}
}
测试类:
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("小黄");
System.out.println(dog.getName());
}
}
继承注意点
- 在Java中,继承只能是单继承,即子类只能有一个父类,而父类可以被多个子类继承。
- 父类无法使用子类的自有成员
- 兄弟类之间也无法共享自有成员
- 子类对象的构建是有父类的构造器参与的
- 但是Java中存在多层继承,常用的比如每个子类只有一个直接父类,但是该父类依然存在其自身的父类。
- super调用父类构造时,一定要放在构造方法的首行上。
- 虽然类继承可以实现代码的复用,但是如果继承的结构过多,也会造成代码段的阅读困难,一般不建议继承的结构超过3层。
总结:
- 一个子类只能有一个父类
- 子类可以访问父类非私有成员
- 子类自己自有成员其他兄弟类无法访问
- 父类不可以访问子类自有成员
方法重写
方法的重写,指的是在子类中重新描述父类中的方法,返回值类型,方法名,参数类型、顺序、个数都要与父类继承的方法一致。当子类重写父类方法之后,子类对象调用的是重写之后的方法。
public class Cat extends Animal{
@Override//注解,表示方法的重写,方法前面加上@Override 系统可以帮你检查方法的正确性
public void eat() {
System.out.println(this.getName() + "爱吃鱼");
}
}
与方法重载的区别
方法重载时发生在同一个类当中,方法重写发生在父类和子类之间。
注:方法的签名是由方法的方法名和形参列表(注意:包含方法的参数和类型)组成
属性可以重写吗
在子类中也是可以定义与父类同名的属性的,此时子类对象调用的是子类的属性。
访问修饰符
访问控制修饰符能修饰的对象包括:属性、方法、构造器。
访问修饰符可以用来限制成员被访问的范围,即用来控制被修饰的成员可以在哪里(包)被使用。
- private: 只允许在本类中进行访问(访问权限最小的)
- public: 允许在任意位置访问(访问权限最大的)
- protected: 允许在当前类、同包子类/非子类、跨包子类调用;跨包非子类不允许
- default: 允许在当前类、同包子类/非子类调用;跨包子类/非子类不允许调用
注:作为访问修饰符的default不可以被显示的写出来;
在不同包的子类中使用父类中被protected修饰的内容,只能由子类对象去调用;
protected常用于父类为子类准备的属性或方法,父类不会去使用这些内容;
访问修饰符对方法重写的影响
子类中重写父类方法时,子类方法的访问修饰符的访问范围需要大于等于父类方法的访问范围
super关键字
想要使用父类的方法,则需要使用super关键字。super关键字代表着对父类对象的引用。也可以使用super访问父类中允许被子类访问到的任意成员。
this代表的是对本类对象的引用。
**
super的总结:
- 子类重写父类的方法,则父类的方法会被隐藏,隐藏的方法或者成员变量可以通过super关键字访问
- 引入super关键字的原因是可以使用被隐藏的成员变量和方法,而且super只能在子类的方法中定义使用
继承的初始化顺序
首先来看下对象的生成顺序:
- new关键字开辟空间
- 执行静态内容
- 执行构造代码块
- 调用构造器
继承后的初始化顺序(实例化子类对象的时候):先加载父类静态成员,然后加载子类静态成员,然后父类对象构造(构造代码块—>属性—>方法),最后子类对象构造。
无论是通过子类的无参或者有参构造器创建子类对象的时候,都会去调用一次父类的无参构造器。
一个类中若是没有无参构造器,则该类无法被继承(在所有子类明确指示不使用父类的无参构造器时,可以不写无参构造器)。
总结:
- 子类默认调用父类无参构造方法;
- 可以通过super()调用父类允许访问的其他构造方法;
- super()必须放在子类构造方法的有效方法的第一行,而且只能出现一次。
- this()和super()在同一构造器中只可能出现一个。
super和this关键字的区别
注:super和this可以同时出现的,而super()和this()不能同时出现在一个构造器中
子类调用父类的构造器情况
- 子类构造器执行体的第一行显式的使用super调用父类的构造器,此时系统将根据super(params)去调用对应的构造器。
- 子类构造器在执行体的第一行显式的使用this调用重载的构造器,系统将会根据this(params) 调用对应的重载构造器,本类中的对应的构造器再去调用父类的构造器。
- 子类构造器中既没有super有没有this,那么子类在执行构造器语句的时候会去执行父类的无参构造器。
- 无论如何子类都会调用一次父类的构造器。
Object类
Object类是所有类的根类,如果有一个类没有显式的说明继承自哪个类,那么该类就默认的继承Object类。
Object类位于java.lang包中。
特点:
- Obejct类是所有类的父类
- 一个类没有使用extends关键字明确标识继承关系,则默认继承Object类(包括数组)
- Java中每个类都可以使用Ojbect类中定义的方法
equals()方法
当直接继承Object类中的equals()方法时,它的作用是判断调用equals()方法的对象的内存空间引用与传入参数对象的内存空间引用是否一致(是否指向的是同一块内存地址)。
在Java中,==不但可以判断两个基本数据类型的数据是佛相等,也可以用来判断两个对象的内存地址是否一致。equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断,所以当我们使用equals方法去判断两个对象的话,发现其作用于==一样,所以需要对equals方法进行重写。
String也是个类,当我们使用equals方法对两个字符串进行判断时为什么可以?这是因为在String类中已经对equals方法进行了重写。
public static void main(String[] args) {
String str = "abc";
//String作为一个类,同样有其构造方法
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str.equals(str1));
System.out.println(str.equals(str2));
System.out.println(str1.equals(str2));
}
运行后我们可以看到equals()方法的返回值都为true,这是因为String类中重写的equals()方法会用来比较保存的字符串内容。
如何重写equals()方法
在父类Animal中重写equals()方法
public boolean equals(Object obj) {
// 1. 首先判断传入的对象是否为null
if (obj == null) {
// 如果是null则直接返回false
return false;
} else {
// 2. 接着判断两个对象中的属性值是否一致
// 此时是引用数据类型的强制类型转换,形式也是类似于之前学习过的基本数据类型强制类型转换
Animal animal = (Animal) obj;
if (this.getName().equals(animal.getName()) && this.getMonth() == animal.getMonth()
&& this.getSpecies().equals(animal.getSpecies())) {
return true;
} else {
return false;
}
}
}
上述方法虽然可以比较父类Animal下子类的对象,但是还是存在漏洞,问题在于强制类型转换的时候,如果传入的参数不能匹配Animal类,则会发生一个类型转换错误的异常。
/**
* 此方法就不是对equals()方法的重写了,而是对Animal类中已经存在的equals()方法的重载
* @param ani
* @return
*/
public boolean equals(Animal ani) {
// 1. 首先判断传入的对象是否为null
if (ani == null) {
// 如果是null则直接返回false
return false;
} else {
// 2. 接着判断两个对象中的属性值是否一致
if (this.getName().equals(ani.getName()) && this.getMonth() == ani.getMonth()
&& this.getSpecies().equals(ani.getSpecies())) {
return true;
} else {
return false;
}
}
}
针对于有可能发生的类型转换异常,我们可以强制限定传入的参数为Animal类型,避免异常的发生。所以,我们重载了一个参数为Animal类型的方法,在测试类中代码运行的时候,会自动根据参数类型定位到类型匹配的重载方法上。
总结:
- 继承Object中的equals()方法时,比较的是两个引用是否指向同一个对象
- 子类可以通过重写equals()方法的形式,改变比较的内容
- 需要注意的是,需要针对传入的参数进行非空判断,避免空指针异常
toString()方法
在没有重写toString()方法之前,对象调用toString()方法会返回一个类的字符串表现形式,这个表现形式就是一个类的类名 + @ + 对象在内存中位置表现的哈希值。
public class Test {
public static void main(String[] args) {
Animal one = new Animal("花花",2,"英短");
Animal two = new Animal("花花",2,"英短");
System.out.println(one);
System.out.println(one.toString());
String str = new String("Hello,world!");
System.out.println(str);
System.out.println(str.toString());
}
}
输出结果
com.dodoke.animal.model.Animal@15db9742
com.dodoke.animal.model.Animal@15db9742
Hello,world!
Hello,world!
在类中重写toString()方法
@Override
public String toString() {
return "Animal [name=" + name + ", month=" + month + ", species=" + species + "]";
}
总结:
- 直接输出对象名时,默认会直接调用类中的toString方法
- 继承Object中的toString()方法时,输出对象的字符串表现形式:类型信息+@+地址信息
- 子类可以通过重写toString()方法的形式,改变输出的内容及其表现形式
final关键字
- final关键字修饰类时,则该类不允许被继承
- final修饰方法,则该方法不允许被重写,但可以正常被子类继承使用
- final修饰变量
- final修饰局部变量,初始化(赋值)后不允许被修改
- final修饰属性变量,同样赋值后不可以修改
被final定义的成员属性,只有三种方式进行赋值:
- 定义时直接赋值;
- 在构造代码块中赋值;
- 在构造方法中赋值。
final当修饰的变量为引用类型时,对象不允许再被重新实例化(不允许再被new),但是此对象里的属性的值依然可以更改。
在实际的开发中, final用的最多的场景是结合 static 关键字定义类变量,即静态变量。定义的这个变量我们也称之为常量。定义为final另一个意图就是将变量的值保护起来。
public class Animal {
public static final int TEMP = 200;
public final static int TEMP1 = 200;
}
注:final修饰的这个常量需要字母全部大写
问:final可以修饰构造方法吗
final不能修饰构造方法!
**
注解
注解是JDK1.5版本引入的一个特性,可以声明在包、类、属性、方法、局部变量、方法参数等的前面,用来对这些元素进行说明、注释。简单来说,注解就相当于一个标记,在程序中使用了注解就相当于给这个程序打上了某种标签。被打过标签的一些程序,以后Java编辑器、开发工具、以及其他的程序就会借由这个标签来了解你所写的程序。
按照运行机制分:
- 源码注解:注解只在源码中存在,编译成.class文件就不存在了。如@Override
- 编译时注解:注解在源码和.class文件中都存在。
- 运行时注解:在运行阶段还起作用,甚至会影响运行逻辑的注解。
按照来源分:
- 来自JDK的注解
- 来自第三方的注解
- 我们自己定义的注解
还有特殊的注解:元注解:对注解进行注释的。