Java面向对象

接口、继承、多态、以及高级特性

Posted by Welt Xing on March 6, 2021

接口、继承和多态

类的继承

Java用extends关键字来标识继承关系:

class Test {
    public Test() {
        TODO();
    }

    protected void doSomething() {
        TODO();
    }

    protected Test doIt() {
        return new Test();
    }
}

class Test2 extends Test {
    public Test2() {
        super();             // 调用父类的构造方法
        super.doSomething(); // 调用父类的成员函数
    }

    public void doSomethingNew() {
        TODO(); // 新的成员函数
    }

    public void doSomething() { // 重写父类方法
        TODONEW();
    }

    protected Test2 doIt() {    // 重写父类方法
        return new Test2();
    }
}
  • 子类可以用super关键字调用父类的方法;

  • 子类没有权限调用父类中被修饰为private的方法,只能是publicprotected

  • 继承还可以重写父类的成员方法:保留名称,修改实现,更改权限,甚至是返回值(但类型必须是同一方法返回值类型的子类);

  • 特殊的重写:函数签名和返回值不变,但内容不同,叫做重构;

  • 重写父类方法时,修改权限只能扩大范围:public > protected > private

  • 在实例化子类对象时,编译器会自动调用父类的默认构造函数,其他构造函数则需要显式调用。

多态

举例即可,无过多细节:

class Base {
    public static void f(Base base) {
        TODO();
    }
}

class Extend1 extends Base {
    ...
}

class Extend2 extends Base {
    ...
}

此时像这样调用函数:

Base.f(new Extend1());
Base.f(new Extend2());

都是可以的,这就是多态,利用这种特性,可以减少在子类中重复编写代码。

抽象类

解决实际问题时常常将父类定义为抽象类,派生类是其的具象化:

public abstract class Test {
    abstract void testbstract();
}
  • 由于抽象的特性,抽象类是无法被实例化对象的,但其子类可以;

  • 抽象方法没有方法体,因为没有意义,但抽象类可以有非抽象方法;

  • 只要类中有抽象方法,它就是抽象类;

  • 抽象类被继承后需要实现其中所有的抽象方法。

接口

背景:Java规定类不能同时继承多个父类,为应对这些问题,接口应运而生。

接口(Interface)算是纯粹的抽象类,因为它的方法必须全是抽象的:

public interface InterfaceTest {
    // 接口内的方法全是抽象的,所以不再需要abstract关键字
    void f();
    void g();
}

一个类实现一个接口,用的是implements关键字:

public class MyClass extends BaseClass implements InterfaceTest {
    ...
}

在接口中定义的任何字段都自动是staticfinal的.

Java中不支持多重继承,但使用接口就可以实现:

class MyClass implements Interface1, Interface2, ...

类的高级特性

Java类包

背景:同一个文件夹中的类不断增加,很容易出现两个名字相同但功能不同的类,这个时候就需要将这两个类放入不同的包里。

一个完整的类名需要包名和类名共同组成,例如我们常用的Math包,导入时要写下:

import java.lang.Math;

这里java.lang就是包的名称,后面的Math是类名。

语法格式

package net.java.util;
class MyClass {
    ...
}

那么它的路径就是net/java/util/MyClass.java

创建包

这里我们不想再依赖IDE,比如Eclipse去帮我们创建包,而是去手动构建。

例子:在文件夹中创建两个文件,Animal.javaMammalInt.java

/* 文件名: Animal.java */
package animals;
 
interface Animal {
   public void eat();
   public void travel();
}
/* 文件名 : MammalInt.java */
package animals;
 
public class MammalInt implements Animal{
 
   public void eat(){
      System.out.println("Mammal eats");
   }
 
   public void travel(){
      System.out.println("Mammal travels");
   } 
 
   public int noOfLegs(){
      return 0;
   }
 
   public static void main(String args[]){
      MammalInt m = new MammalInt();
      m.eat();
      m.travel();
   }
}

然后我们在终端做如下处理:

> mkdir animals # 对应包名
> cp Animal.class MammalInt.class animals # 将文件移到与包名相同的子文件夹中
> java animals/MammalInt # 编译运行
Mammal eats
Mammal travel

例子摘自https://www.runoob.com/java/java-package.html

导入包和import关键字

import java.lang.Math;
import java.lang.*

在Java中将Java源文件与类文件放在一起管理是极为不好的管理方式。我们可以在编译时使用-d参数设置编译后类文件产生的位置:

javac -d ./bin/ ./xyz/welts/*.java

这样编译成功后将在当前运行路径下的bin目录中产生xyz/welts路径,并在该路径下出现相应的类文件。

我们还可以使用import导入静态成员,使我们编程更加方便:

import static java.lang.System.out;

public class Main {
    public static void main(String[] args) {
        out.println("Hello world");
    }
}

如此,我们就可以减少代码的书写量。

package的目录结构

下面是管理自己java文件的一种简单方式:

将类,接口等类型放在一个文本中,这个文件的名字就是这个类型的名字,并且以.java作为扩展名:

package matriculate;

public class Matrix {
    ...
}

接下来将源文件放在一个目录中,这个目录要对应所在包的名字:

./matriculate/Matrix.java

现在,正确的类名和路径将会是如下样子:

  • 类名:matrixculate.Matrix

  • 路径名:matrixculate/Matrix.java

通常,一个公司使用它互联网域名的颠倒形式来作为它的包名.例如:互联网域名是welts.xyz,所有的包名都以xyz.welts开头。包名中的每一个部分对应一个子目录。

例如有个叫xyz.welts.matriculate的包,这个包包含一个叫Matrix.java的源文件,那相应的,应该有如下一连串子目录:

./xyz/welts/matriculate/Matrix.java

这时再用上面所说-d选项编译文件,可以放在同一文件夹,也可以放在不同文件夹以便于分类管理。

final变量

我们在之前已经接触过final变量:

final int x = 10;

事实上,final也可以修饰对象引用,当一个对象引用被修饰为final后,他只能恒定指向一个对象,无法改变其指向另一个对象(类似于C++中的Type* const ptr),一个既是static又是final的字段只占据一段不能改变的存储空间:

public final Matrix A = new Matrix(3, 3);

只在一个对象中无法改变,每个对象都有自己不变的A;

public static final Matrix A = new Matrix(3, 3);

被所有对象共享,而且不变,所以只占据一段不变的内存空间。

在Java中定义全局变量,通常使用public static final修饰,这样的常量只能在定义时被赋值。

final方法和final类

final方法就是不能被override的方法,final类就是不能被继承的类。

我们在前面提过,父类中的private方法没有对子类开放,所以它被默认为final方法,因此就不存在private final这种写法。

final类的所有方法都被隐式设置为final形式,但是final类中成员变量可以被定义成final或非final形式。

内部类

成员内部类

public class OuterClass {

    private Type MemberVar;

    public class InnerClass {
        ... // 你可在这里使用`MemberVar`
    }
    ...
}

此时可以在内部类中直接存取其所在类的私有成员变量和方法,反之不然,但可以在外部类通过内部类对象引用调用成员变量。

局部内部类

你还可以像这样定义一个类:

class OuterClass {
    public int function(final String x) {
        class InnerClass {
            InnerClss(String s) {
                s = x;
                System.out.println(s);
            }
        }
        return new InnerClass("Hello");
    }
}

作用域告诉我们,此时OuterClass类中其他部分无法使用InnerClass;此外,我们发现function的参数是final类型,因为在方法中定义的内部类只能访问方法中的final类型局部变量

匿名内部类

我们来看一个有趣的程序:

class OuterClass {
    public InnerClass function() {
        return new InnerClass() {
            private int i = 0;
            public int getValue() {
                return i;
            }
        };
    }
}

这就是一个匿名内部类的创建,由于匿名函数没有名称,所以所有匿名内部类使用默认构造方法来生成OutClass对象。