07 – 08. Java学习 – 继承、抽象类、final关键字、接口与多态
前言
继承:子类使用父类的方法
多态:父类使用子类的方法
一、 继承
所谓继承,就是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。
1. 继承的定义格式
**继承的定义格式:**
public class 父类{
... ...
}
public class 子类 extends 父类 {
... ...
}
继承关系下,父类公有的属性和方法都会被子类共享。
/*例子:*/
// 父类
public class Animal {
public String name;
public int age;
public void showInfo(){
System.out.println("姓名:" + this.name + " 年龄:" + this.age);
}
}
// 子类 extends继承的关键字
public class Cat extends Animal{
}
// 测试类
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat();
cat.name = "橘猫";
cat.age = 2;
cat.type = "田园猫";
cat.showInfo(); // 姓名:橘猫 年龄:2
}
}
2. 方法的重写
// 例子:
// 父类
public class Animal {
public String name;
public int age;
public String type;
public void show(){
System.out.println("animal");
}
}
// 子类
public class Cat extends Animal{
@Override // 方法重写的标识 本身这个注解没有任何的实际作用
public void show() {
System.out.println("cat");
}
}
方法重写要注意:
- 子类重写的方法,要求方法的访问修饰符必须大于或等于父类
- 在方法重写时,要求子类方法的返回值类型必须要小于或等于父类
- 子类方法抛出的异常类型必须小于或等于父类方法抛出的异常类型
2.1. 方法重写和方法重载的区别
- 定义不同:重载是定义相同的方法名,但参数列表不同;重写是子类重写父类方法
- 范围不同:重载是在一个类中;重写是在继承关系的前提下
- 参数不同:重载的参数个数、类型、顺序可以不同;重写必须严格相同
- 修饰不同:重载对访问修饰符范围没有要求;重写要求访问修饰符范围大于或等于被重写方法
3. 使用继承的注意事项
3.1. private关键字在继承中的使用
priavte关键字修饰的资源是不能被继承的。
如果我们想要在子类中访问父类的私有成员变量,只能在父类中提供公有的set、get方法。
3.2. super关键字在继承中的使用
super关键字只能在继承的环境下面使用
定义类时,如果子类和父类中出现了同名的变量,在子类使用这个变量的时候,java编译器会按照就近原则,使用子类自己内部的变量。如果我们就是想要访问父类中的同名属性,就可以使用super关键字。
**使用格式:**
super.变量名 // 指定访问父类的成员变量
如果子类和父类中有同名的方法,super也可以调用父类的方法。
super.方法名 // 指定访问父类的方法
super关键字还可以调用父类的构造函数(注意:父类的构造函数是不能被子类继承的,只能被子类调用)
// 父类
public abstract class Poultry {
private String name;
private String symptom;
private int age;
private String illness;
public Poultry(String name, String symptom, int age, String illness) {
this.name = name;
this.symptom = symptom;
this.age = age;
this.illness = illness;
}
}
// 子类
public class Duck extends Poultry {
public Duck(String name, String symptom, int age, String illness) {
super(name,symptom,age,illness);
}
}
super和this关键字的区别
- this.变量名称
- 在一个类的里面,如果成员变量和局部变量同名,this.变量名,指定使用成员变量
- 在继承关系下,在子类中使用this.变量名,先找子类的成员变量,如果找不到再找父类的成员变量
- this.方法名称
- 在一个类里面,this.方法名称,先从子类找这个方法,如果找不到再找父类中的方法
- 在一个类里面,构造函数的重载,在一个构造函数的内部,可以使用this去指代另一个构造函数
- super
- 不管是调用变量还是调用方法,都是访问父类的方法
- 如果父类定义构造函数,那么在子类的内部必须通过super关键字调用父类的构造函数
- 多个继承关系,super关键字只能找到子类的直接父类
二、抽象类
在描述一类事物的时候,发现该事物确实存在着某种行为,但是目前该行为是不具体的,那么这时候我们应该抽取该方法的声明,而不去实现该方法。
1. 抽象类的定义格式
public abstract class 类名{
public abstract 返回值类型 方法名(参数列表);
}
// 例子:
// 类名必须使用abstract关键字修饰
public abstract class Shape {
// 抽象方法也必须使用abstract关键字修饰
public abstract double getArea();
}
要注意,抽象类是不能直接使用的,也就是说我们不能创建抽象类的实例化对象。抽象类定义之后,必须要被其子类继承,里面的方法必须由其子类重写之后才能使用。
2. 使用抽象类的注意事项
- 如果具体的子类继承了一个抽象类,在当前子类里面必须重写父类的所有的抽象方法
- 如果一个抽象的子类继承一个抽象的父类,在抽象的子类里面可以重写,也可以不重写父类的抽象方法
- 在一个抽象类里面可以存在非抽象的方法
- 抽象类不能使用new关键字实例化对象
- 抽象类可以有成员变量和构造函数,因为提供了构造函数可以被其子类调用
三、final关键字
final意味着不可改变,final可以用来修饰类、方法和变量
- final修饰类意味着该类不可被继承
- final修饰方法意味着该方法不可被重写
- final修饰变量意味着该变量不可被修改,是常量
1. final关键字的使用
1.1. final关键字修饰类
final修饰类意味着该类不可被继承
** 使用格式:**
访问修饰符 final class 类名{}
// 例子:
public final class FinalTest{
String name;
int age;
public void showInfo(){
System.out.println("这是一个final类");
}
}
Java中也定义了很多final修饰的类,例如:String、Math、Scanner…
1.2. final关键字修饰方法
final修饰方法意味着该方法不可被子类重写
** 使用格式:**
访问修饰符 final 返回值 函数名(){}
// 例子:
public class FinalTest{
String name;
int age;
public final void showInfo(){
System.out.println("这是一个final方法");
}
}
1.3. final关键字修饰变量
final修饰变量意味着该变量不可被修改,是常量
**使用格式:**
final 数据类型 变量名 = 赋值;
// 例子:final修饰局部变量
public class FinalTest{
String name;
int age;
public final void showInfo(){
final int a = 20;
System.out.println(a + "是一个final变量");
}
}
// 例子:final修饰成员变量
public class FinalTest{
final String name;
int age;
public final void showInfo(){
System.out.println(name + "是一个final变量");
}
}
四、接口
1. 接口的定义格式
**使用格式:**
public interface 接口名称{ // 抽象方法 }
2. 接口的使用
类实现接口的关键字是implements
/*例子:*/
// 接口1
interface InterfaceTest1 {
default void show(){
System.out.println("接口1被default修饰的show方法");
}
}
// 实现类
public class Main implements InterfaceTest2{
public static void main(String[] args) {
}
}
3. 定义接口的注意事项
3.1. 接口中可以定义抽象方法,也可以定义非抽象方法
jdk8后,Java允许接口中定义非抽象方法。在jdk8之前,接口中只能定义抽象方法。
// 例子:
public interface Shape {
double getArea();
public abstract double getLength();
//定义非抽象的方法 非抽象方法在接口中定义,访问修饰符以default开头
default void defaultMethod(){
System.out.println("hello");
}
}
3.1.1. 接口中关于default关键字的使用
default 关键字在jdk8后被引入,属于修饰符关键字,default关键字大部分都用于修饰接口。
default 关键字在修饰方法时只能在接口类中使用,在接口中被 default 标记的方法可以直接写方法体,而无需修改所有实现了此接口的类。
- 1. default关键字是全局的,可以在不同接口中定义一个相同的方法
/*例子:*/
// 接口1
interface InterfaceTest1 {
default void show(){
System.out.println("接口1的default修饰的show方法");
}
}
// 接口2
interface InterfaceTest2 {
default void show(){
System.out.println("接口2的default修饰的show方法");
}
}
public class Main {
public static void main(String[] args) {
}
}
- 2. 可以在实现类中单独实现任意一个接口的同名方法
/*例子:*/
// 接口1
interface InterfaceTest1 {
default void show(){
System.out.println("接口1被default修饰的show方法");
}
}
// 接口2
interface InterfaceTest2 {
default void show(){
System.out.println("接口2被default修饰的show方法");
}
}
// 单独实现接口1的show方法
/*public class Main implements InterfaceTest1{
public static void main(String[] args) {
new Main().show(); // 接口1被default修饰的show方法
}
}*/
// 单独实现接口2的show方法
public class Main implements InterfaceTest2{
public static void main(String[] args) {
new Main().show(); // 接口2被default修饰的show方法
}
}
- 3. 在实现类中同时实现2个接口
- 在实现类中重写同名方法
- 用一个 @Override 告诉编译器要运行的是哪个接口中的方法
/* 例子:在实现类中重写同名方法 */
// 接口1
interface InterfaceTest1 {
default void show() {
System.out.println("接口1被default修饰的show方法");
}
}
// 接口2
interface InterfaceTest2 {
default void show() {
System.out.println("接口2被default修饰的show方法");
}
}
// 实现类
public class Main implements InterfaceTest1, InterfaceTest2 {
@Override
public void show() {
System.out.println("实现类中重写的show方法");
}
public static void main(String[] args) {
new Main().show(); // 实现类中重写的show方法
}
}
/*例子:指定实现接口*/
interface InterfaceTest1 {
default void show() {
System.out.println("接口1被default修饰的show方法");
}
}
// 接口2
interface InterfaceTest2 {
default void show() {
System.out.println("接口2被default修饰的show方法");
}
}
// 实现类
public class Main implements InterfaceTest1, InterfaceTest2 {
@Override
public void show() {
InterfaceTest1.super.show();
InterfaceTest2.super.show();
}
public static void main(String[] args) {
new Main().show(); // 接口1被default修饰的show方法
// 接口2被default修饰的show方法
}
}
- 4. 子类继承父类,父类中有show方法,该子类同时实现的接口中也有show方法(被default修饰)
- 在实现类中重写同名方法
/*例子:在实现类中重写同名方法*/
// 接口1
interface InterfaceTest1 {
default void show() {
System.out.println("接口1被default修饰的show方法");
}
}
// 父类
class Father{
void show(){
System.out.println("这是父类的show方法");
}
}
// 实现类
public class Main extends Father implements InterfaceTest1 {
@Override
public void show() {
System.out.println("被实现类重写后的show方法");
}
public static void main(String[] args) {
new Main().show(); // 被实现类重写后的show方法
}
}
3.2. 接口中不能定义成员变量,如果要定义,必须定义成常量
// 例子:
interface InterfaceTest1 {
/* String name; 编译器报错 */
// 定义一个常量,编译器在编译时会自动加上public static final 变为 public static final int age = 22
int age = 22;
default void show() {
System.out.println("接口1被default修饰的show方法");
}
}
3.2.1. 接口中关于final关键字的使用
- 1. 接口不可以被final修饰
关于这一点,我在学习的时候看到有人说:接口可以被final修饰。
但是,我自己尝试的时候报错,编译器说不允许用final修饰。再查资料,又看到有人说接口不能被final修饰,就先按我自己的尝试结果,当他不能吧。
- 2. 接口的成员变量必须被final修饰,变成常量
- 3. 接口的方法不能用final修饰
3.3. 类与类之间只能是单继承,但是接口与接口之间是可以多继承的
/*例子*/
interface InterfaceTest1 {
default void show() {
System.out.println("接口1被default修饰的show方法");
}
}
// 接口2
interface InterfaceTest2 {
default void show() {
System.out.println("接口2被default修饰的show方法");
}
}
// 实现类
public class Main implements InterfaceTest1, InterfaceTest2 {
@Override
public void show() {
InterfaceTest1.super.show();
InterfaceTest2.super.show();
}
public static void main(String[] args) {
new Main().show(); // 接口1被default修饰的show方法
// 接口2被default修饰的show方法
}
}
总结:
- 类与类之间只能是单继承
- 接口与接口之间可以有多继承
- 类与接口之间是实现关系
五、多态
多态按字面意思就是“多种状态”,让继承自同一父类的子类们,在执行相同方法时有不同的表现(状态)。多态所解决的问题是让同一个对象有唯一的行为特征。
1.多态的定义格式
这里可以扩展一个知识点:里氏替换原则。
里氏替换原则是面向对象七大原则中最重要的原则,其概述为任何父类出现的地方,子类都可以替代,作用是方便进行对象的储存和管理。
有概念说:里氏代换原则是对“开-闭”原则的补充,实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范,其要求子类从抽象继承而不是从具体继承。若是从抽象中继承,那么子类必然要重写父类方法,因此里氏转换原则和多态相辅相成。
里氏替换原则的语法表现为:
父类容器装载子类对象,因为子类对象包含了父类的所有内容
1.1. 继承下的多态
**定义格式:**
父类的数据类型 变量名称 = new 子类的数据类型();
// 例子:
// 定义一个父类
public class Person {
public void show(){
System.out.println("父类Person");
}
}
// 定义一个子类
public class Student extends Person{
@Override
public void show() {
System.out.println("子类Student");
}
}
// 测试类
public class TestStudent {
public static void main(String[] args) {
Person student = new Student(); // 多态
student.show(); // 学生边看书边吃饭.....
}
}
1.2. 接口下的多态
**定义格式:**
接口的数据类型 变量名称 = new 实现类的数据类型();
// 例子:
// 接口
public interface UserDao {
public void add();
}
// 实现类
public class UserDaoImpl implements UserDao{
@Override
public void add() {
System.out.println("实现类");
}
}
// 测试类
public class TestUserDao {
public static void main(String[] args) {
UserDao userDao = new UserDaoImpl(); // 多态
userDao.add();
}
}
2. 多态的使用
2.1. 将父类作为方法的形式参数
将父类作为方法的形式参数,可以接收更多的数据类型
例子:封装一个计算图形面积的方法
// 父类
public abstract class Shape {
public String name;
public Shape(String name) {
this.name = name;
}
//计算图形面积的方法
public abstract double getArea();
}
// 子类
public class Square extends Shape{
int width;
public Square(String name) {
super(name);
}
public Square(String name, int width) {
super(name);
this.width = width;
}
@Override
public double getArea() {
double area = width * width;
return area;
}
}
public class Circle extends Shape{
int r;
public Circle(String name, int r) {
super(name);
this.r = r;
}
@Override
public double getArea() {
return 3.14*this.r*this.r;
}
}
// 测试类
public class TestShape {
public static void main(String[] args) {
Square square = new Square("正方形",4);
System.out.println(getAreaFromShape(square));
}
public static double getAreaFromShape(Shape shape){
return shape.getArea();
}
}
2.2. 将父类作为方法的返回值
将父类作为方法的返回值,可以返回更多的数据结果。
例子:根据用户输入的数字,返回图形
// 父类同上
// 测试类
public class TestShape {
public static void main(String[] args) {
Shape shape = getShape(2);
System.out.println("图形名称是:" + shape.name);
}
//将父类作为方法的返回值可以返回更多类型的数据
public static Shape getShape(int num){
Shape shape = null;
if(num == 1){
// 如果用户输入1返回正方形
shape = new Square("正方形",10);
}
if(num == 2){
// 如果用户输入2返回圆形
shape = new Circle("圆形",3);
}
return shape;
}
}
3. 使用多态的注意事项
1. 多态场景下面对成员变量的使用
* 如果子类和父类存在同名的成员变量,在多态场景下面,访问的是父类的成员变量的值
例子:
// 父类
public class Father {
int num =10;
public void show(){
System.out.println("父类");
}
}
// 子类
public class Son extends Father{
int num = 100;
int a = 10;
public void show(){
System.out.println("子类");
}
}
// 测试类
class TestSon{
public static void main(String[] args) {
/**
* 在多态的前提下,如果父类和子类都存在同名的成员变量,访问的父类的成员变量的值。
*/
Father f = new Son();
System.out.println(f.num); // 访问的是父类的num值
}
}
2. 多态场景下面对成员方法的调用
* 如果子类和父类存在同名的成员方法,在多态的场景下面,访问的是子类的成员方法
// 测试类
class TestSon{
public static void main(String[] args) {
Father f = new Son();
/**
* 在多态的前提下,如果父类和子类都存在同名的成员方法,访问的是子类的成员方法
*/
f.show();
}
}
3. 如果父类是抽象类,也存在多态的场景
// 父类
public abstract class Shape {
public abstract void getArea();
}
// 子类
public class Square extends Shape{
@Override
public void getArea() {
System.out.println("计算正方形的面积");
}
}
// 测试类
class TestShape{
public static void main(String[] args) {
Shape shape = new Square(); // 多态
shape.getArea();
}
}
4. 在多态场景下面,子类特有的方法是不能被直接访问
// 父类
public class Father {
int num =10;
public void show(){
System.out.println("父类");
}
}
// 子类
public class Son extends Father{
int num = 100;
int a = 10;
public void show(){
System.out.println("子类");
}
// 子类中特有的方法
public void work(){
System.out.println("子类特有的方法");
}
}
class TestSon{
public static void main(String[] args) {
Father f = new Son();
f.show();
// f.work(); // 调用子类特有的方法,此时编译报错
// 对象 instanceof 类 --> 用来判断某个对象是否属于某个类型
if(f instanceof Son){
((Son) f).work(); //就可以调用子类中特有的方法
}
}
}
补充
一点开发的“潜规则”:
- 操作数据库相关:
- 接口命名一般为:XxDao
- 实现接口的类命名一般为:XxDaoImpl
- 业务层相关:
- 接口命名一般为:XxService
- 实现接口的类命名一般为:XxServiceImpl
IDEA快捷方式:
- 快速生成需要重写的方法:Ctrl+O
- 快速创建对象:.var
- new Random.var --> Random random = new Random();
- 快速将代码用输出语句包裹:.sout
- myList.show().sout --> System.out.println(myList.show());