面向对象的三大特性是封装、继承和多态。封装是将属性与外界隔离,同时由方法作为与外部的接口。而继承则是要解决代码重用的问题,利用继承可以从当前类派生出新的子类,也可以利用子类扩展更多的方法。

继承

继承实现

首先继承的语法为:

class Student extends Person {}

上面的语法能够从Person这个类中派生出Student这个类,也就是说JAVA中继承使用的是关键字extends。

看下边一段代码:

class Person {
    private String name;
	private int age;
	
	Person() {
	}
	
	Person(String name,int age) {
	    this.name = name;
		this.age = age;
	}
	
	void getInfo() {
	    System.out.println("Name is " + getName() + ",age is " + getAge());
	}
	
	public String getName() {
	    return name;
	}
	
	public int getAge() {
	    return age;
	}
	
	public void setName(String name) {
	    this.name = name;
	}
	
	public void setAge(int age) {
	    this.age = age;
	}
}

class Student extends Person{
	private int grade;
	
	Student() {
	}
	
	Student(String name,int age,int grade) {
	    super(name,age);
		this.grade = grade;
	}
	
	void getInfo() {
	    System.out.println("Name is " + getName() + ",age is " + getAge() + ",grade is " + getGrade());
	}
	
	public int getGrade() {
	    return grade;
	}
	
	public void setGrade(int grade) {
	    this.grade = grade;
	}
}

public class Demo {
    public static void main(String args[]) {
	    Student st = new Student();
		st.setName("wang");
		st.setAge(25);
		st.setGrade(5);
		st.getInfo();
	}
}

结果为:

Name is wang,age is 25,grade is 5

上面的代码可以看出:

  • 子类中可以添加父类中没有的属性和方法
  • 子类会继承父类中的属性和方法
  • 子类可以使用super方法调用父类的构造方法
  • 父类中的private属性,子类不能直接访问,只能通过getter方法访问,这点和C/C++不同
  • 子类会覆盖父类中不会产生重载的方法

继承限制

JAVA的语法区别于C/C++,在继承中也存在许多不同。这也就是JAVA对于继承所作的限制:

  • JAVA不允许多重继承,但是允许多层继承
  • 子类继承父类时,会继承父类中的全部操作,但是对于所有的私有操作属于隐式继承,而所有的非私有操作属于显示继承
  • 在子类对象构造前默认会调用父类的构造方法(默认使用无参构造),以保证先实例化父类对象,再实例化子类对象

这里进行一一说明,首先是多层继承,这点区别于C/C++。C/C++中能够进行多重继承,但是JAVA却抛弃了这个特性:

class Person {
    private String name;
	private int age;
	
	Person() {
	}
	
	Person(String name,int age) {
	    this.name = name;
		this.age = age;
	}
	
	void getInfo() {
	    System.out.println("Name is " + getName() + ",age is " + getAge());
	}
	
	public String getName() {
	    return name;
	}
	
	public int getAge() {
	    return age;
	}
	
	public void setName(String name) {
	    this.name = name;
	}
	
	public void setAge(int age) {
	    this.age = age;
	}
}

class Student extends Person{
	private int grade;
	
	Student() {
	}
	
	Student(String name,int age,int grade) {
	    super(name,age);
		this.grade = grade;
	}
	
	void getInfo() {
	    System.out.println("Name is " + getName() + ",age is " + getAge() + ",grade is " + getGrade());
	}
	
	public int getGrade() {
	    return grade;
	}
	
	public void setGrade(int grade) {
	    this.grade = grade;
	}
}

class Undergraduate extends Student {
    private String major;
	
	Undergraduate() {
	}
	
	Undergraduate(String name,int age,int grade,String major) {
	    super(name,age,grade);
		this.major = major;
	}
	
	void getInfo() {
	    System.out.println("Name is " + getName() + ",age is " + getAge() + ",grade is " + getGrade() + ",major is " + getMajor());
	}
	
	public String getMajor() {
	    return major;
	}
	
	public void setMajor(String major) {
	    this.major = major;
	}
}

public class Demo {
    public static void main(String args[]) {
	    Undergraduate st = new Undergraduate();
		st.setName("wang");
		st.setAge(25);
		st.setGrade(5);
		st.setMajor("History");
		st.getInfo();
	}
}

结果为:

Name is wang,age is 25,grade is 5,major is History

虽然JAVA允许多层继承,但是也不要过度使用,否则会造成继承关系过于复杂。

再往下看,C/C++父类中的私有属性被子类继承后仍然可以在子类方法内部访问,但是JAVA中却并不是如此。JAVA并不希望子类直接访问父类的私有属性,因此才要统一构建setter和getter方法作为访问和设置其属性的接口。这点之前已经提到过了,因此才称私有操作为隐式继承,只能通过setter和getter进行间接访问,而非私有操作则为显式继承,可以直接在类中访问。

最后再看一下父类和子类的构造方法:

class A {
    public A() {
	    System.out.println("Constructor A");
	}
}

class B extends A {
    public B() {
	    System.out.println("Constructor B");
	}
}

public class Demo {
    public static void main(String args[]) {
	    B tmp = new B();
	}
}

结果为:

Constructor A
Constructor B

上面的执行结果和C/C++原理类似,先执行父类的构造方法,然后执行子类的构造方法。而如果父类只存在有参构造或者是在子类中想要调用父类的有参构造则需要:

class A {
    public A() {
	    System.out.println("Constructor A");
	}
	
	public A(String var) {
	    System.out.println("Constructor A " + var);
	}
}

class B extends A {
    public B() {
	    System.out.println("Constructor B");
	}
	
	public B(String var1,String var2) {
		//super(var1);
	    System.out.println("Constructor B " + var1 + " " + var2);
	}
}

public class Demo {
    public static void main(String args[]) {
	    B tmp = new B("Hello","world");
	}
}

结果为:

Constructor A
Constructor B Hello world

如果去掉上方的注释,结果为:

Constructor A Hello
Constructor B Hello world

也就是说,如果没有用super方法显式说明父类所调用的构造方法,那么就默认调用父类的无参构造方法,而如果想要显式调用父类的其它构造方法,就需要使用super方法显式进行调用。而由于super方法主要调用的是父类的构造方法,因此必须放在子类构造方法的首行。

比如上面的代码,如果是想要调用无参构造方法,那么子类构造方法首行的super()写与不写是没有区别的,而如果想要调用有参的构造方法,则需要显式调用super(var)。这样考虑的话,如果父类中存在自定义的构造方法,也就是自定义的构造方法覆盖了默认的无参构造方法,此时在子类中如果想要调用父类的构造方法就需要显式调用super(var),因为父类并没有默认的构造方法,当然需要显式调用super方法。

因此为了避免程序出现错误,最好在每个类中都设定默认的无参构造方法。

覆写

继承是为了解决代码重用的问题,而继承同时也可以对父类进行功能扩展,功能扩展主要是借用覆写来实现的。

方法覆写

子类定义的方法和父类方法名称、返回值类型、参数类型和个数完全相同的情况时,称为方法覆写。否则该同名方法应该称为方法重载。

class A {
    public void fun() {
	    System.out.println("fun A");
	}
	
	public void func(String var) {
	    System.out.println("func A");
	}
}

class B extends A {
    public void fun() {
	    System.out.println("fun B");
	}
	
	public void func(String var1,String var2) {
	    System.out.println("func B");
	}
}

public class Demo {
    public static void main(String args[]) {
	    B tmp = new B();
		tmp.fun();
		tmp.func("Hello");
		tmp.func("Hello","world");
	}
}

结果为:

fun B
func A
func B

从上面的结果来看,子类的fun会覆盖父类的fun,也就是方法覆写,而子类的func和父类的func同时存在,也就是方法重载。对于子类来说,同时存在两个func方法,分别有一个和两个参数。

对于方法覆写来说,调用的一定是子类的方法,而方法重载则会根据参数情况不同而调用父类或者子类的同名方法。

但是子类所覆写的方法不能拥有比父类更为严格的访问控制权限。这里要先说明一下访问控制权限,这点和C/C++类似。C/C++中存在三种权限,分别是private/protected/public,而JAVA中也存在三种权限,从宽到严分别是public/default(默认,不写)/private。也就是说,父类的public方法被覆写后只能是public,而父类的default方法被覆写后可以是public或default。

class A {
    void fun() {
	    System.out.println("fun A");
	}
}

class B extends A {
    public void fun() {
	    System.out.println("fun B");
	}
}

public class Demo {
    public static void main(String args[]) {
	    B tmp = new B();
		tmp.fun();
	}
}

结果为:

fun B

上面的方法覆写中,权限关系是合理的,而反过来则是错误的。而虽然方法覆写扩展方法的访问控制权限从语法上讲是合理的,但是从原则上讲,如果private方法覆写为了public方法,似乎并不合理。因此最好还是要保证类属性是private,方法是public的为好。

这里再看一段代码:

class A {
    public void fun() {
	    this.func();
	}
	
	public void func() {
	    System.out.println("func A");
	}
}

class B extends A {
	public void func() {
	    System.out.println("func B");
	}
}

public class Demo {
    public static void main(String args[]) {
	    B tmp = new B();
		tmp.fun();
	}
}

结果为:

func B

上面实例化了类B,然后调用fun方法,此时会执行this.func(),而此时的func由于存在方法覆写,因此调用的是子类的func方法。而如果将父类的func方法修改为private:

class A {
    public void fun() {
	    this.func();
	}
	
	private void func() {
	    System.out.println("func A");
	}
}

class B extends A {
	public void func() {
	    System.out.println("func B");
	}
}

public class Demo {
    public static void main(String args[]) {
	    B tmp = new B();
		tmp.fun();
	}
}

此时的结果为:

func A

这样解释的话,之前将private方法用覆写的形式扩展访问权限为public就不能称之为方法覆写了,因为如果这样的形式存在的话,上面的代码结果就不应该是func A了,也就是说private权限的方法无法覆写。此时对于子类而言,就相当于定义了一个新的同名方法,不过从形式上看是覆写的形式罢了。这点可以通过下面一段代码来说明:

class A {
    public void fun() {
	    this.func();
	}
	
	public void func() {
	    System.out.println("func A");
	}
}

class B extends A {
	public void func() {
		super.func();
	    System.out.println("func B");
	}
}

public class Demo {
    public static void main(String args[]) {
	    B tmp = new B();
		tmp.fun();
	}
}

结果为:

func A
func B

上面的代码是合理的,而如果将父类的func方法修改为private则是错误的,这也说明了两点:

  • super类似于this,不过this指代当前对象,super指向父对象
  • private方法不能覆写,因为super.func()的执行结果是错误的,也正说明了当前类对象的父类中不存在public的func,不能够直接访问

属性覆盖

和方法覆写类似,如果子类定义了和父类完全相同的属性名称时,就称为属性覆盖。

  • 子类属性会覆盖父类的同名属性
  • 子类中可以使用super来访问父类的同名属性

不过总的来说,属性覆盖并没有什么用,通常也不会使用到。

final

上面提到了继承,可以从父类中派生出子类。而如果不想要该类继续被派生,就可以使用final修饰。JAVA中可以使用final定义类、方法和属性。

  • final定义的类不能被继承,也就不能有子类
  • final定义的方法不能被子类所覆写
  • 使用final定义的变量就成了常量,常量必须在定义的时候进行赋值,并且不能修改

final定义常量有点类似C/C++中的const用法,而由于final修饰的变量不能够被修改,因此多用大写。除此之外还存在“全局常量”的概念,所谓全局常量就是用“public”、“static”、“final”联合修饰的变量。之前提到的String类就是final修饰的,因此不能够被继承。