Java 面向对象编程基础学习记录

2020-08-13
·
39 min read
a cup of hot coffee, with the text 'java' on the cup

黑历史文章系列

对象与类

简介

Class

类相当于模板或者蓝图,所有的对象都从类中创建而来,当从类中构建一个对象时,就称构建了类的实例

在操作对象时最重要的原则是封装 (Encapsulation), 即信息的隐藏,对象的数据称为 instance fields,操作对象的数据的过程叫做 methods,即面向过程编程中称的函数,我们应该仅仅通过调用对象的方法来与对象进行交互。

Java 中的所有类都继承自一个超类 Object

Object

了解清楚对象的三个关键特征:

  • 对象的行为 (behavior):调用什么方法来与对象交互?
  • 对象的状态 (state):当调用方法时对象会做出什么反应?
  • 对象的身份 (identity):某个对象如何与另一个有着相同行为和状态的对象区分开?

所有源自同一个类的对象共享相同的方法,但各自持有不同的状态。

类之间的关系

  • Dependece (use)
  • Aggregation (has)
  • Inheritance (is)

Predifined Class

类似于 Math 这样的类,可以直接调用其方法而不需要创建其实例,仅仅只是功能性地起作用的方法,不需要对其对象的数据进行操作,这样的类并非典型的类,接下来对 Date 类进行具体讲解来了解对象。

对象与对象变量

使用构造方法来创建实例,构造方法是一类特殊的方法,用来构造并初始化对象。构造方法始终与类同名,并且使用 new 关键字,例如 new Date() 便创建了一个日期对象指示当前的时间,并且可以将该对象传递给其它的方法,或者调用该对象的方法

System.out.println(new Date());
String day = new Date().toString();

同样地可以将创建的对象存储在变量中以便多次调用

Date now = new Date();

如果仅仅使用 Date now; 声明了一个 Date 类型的变量,那么此时这个变量 now 并没有指向任何对象,因此无法对其调用任何方法,必须首先初始化。

必须注意的是,一个对象变量并不包含任何对象,仅仅是指向对象而已。

可以手动将对象变量赋值为 null 指明当前没有引用任何对象。

使用本地时间类 LocalDate 可以更方便的操作时间对象,使用静态工厂方法来创建该类的对象

LocalDate now = LocalDate.now();
LocalDate time = LocalDate.of(1998, 5, 7)

对其调用 getYear 等方法可以返回相应信息

Mutator And Accessor Methods

观察下列示例

LocalDate now = LocalDate.now();
LocalDate time = now.plusYears(10);

now 变量调用 plusYears 方法后,其本身的值并未发生改变,只是返回了一个新的对象,这样的方法并不会改变 (mutate) 被调用对象的值。相反地,如果某个对象在调用某个方法后自身的数据发生了改变那么这个方法就称作 mutator method

如果某个方法只是访问对象而不修改对象,那么这个方法就称作 accessor method

定义新类

通常一个类包含以下的结构

class ClassName {
    // fields
    // constructors
    // methods
}

首先定义字段,定义构造方法,再定义其它方法,如下的例子

class Animal {
    private String name;
    private int age;

    public Animal(String n, int a) {
        name = n;
        age = a;
    }

    public String getName() {
        return name
    }

    public int getAge() {
        return age;
    }
}
Animal a = new Animal("Pickle", 2);
System.out.printf("Name: %s Age: %s", a.getName(), a.getAge());

类中定义的所有的方法都被声明为 public,表明任何类中的任何方法都可以调用此方法,而字段中的变量被声明为 private,表明只有该类的内部的方法才能访问这些变量,而外部方法无法读写这些变量的值

构造方法

构造方法的名称与类的名称保持一致,用于初始化 instance fields 中的变量,并且构造方法只有再使用 new 关键字时才会调用,不能对已存在的对象再次调用构造方法。构造方法的特点总结如下:

  • 名称与类名一致
  • 一个类可以有多个构造方法
  • 可以接收不受数量限制的参数
  • 没有返回值
  • 总是在使用 new 时被调用

注意下图中的错误,不要在构造方法中再次声明相同名称的局部变量,建议调用字段时加上 this

public Animal(String n, int a) {
    String name = n;
    int age = a;
}

使用 var 关键字

使用 var 关键字声明局部变量,可以不用指明变量类型而自动推导类型,但注意的是它只能用来声明局部变量,在指定参数或 instance fields 时仍然需要指明变量类型

var a = new Animal("Pickle", 2);

null 引用

当某个对象未初始化值时,若继续对其调用方法,会出现空指针异常 (NullPointerException),如果未对其进行处理程序就会立即终止,因此为了避免在初始化对象时某些值为空,需要预先进行判定,如下例在构造方法中进行判定设置默认值

public Animal(String n, int a) {
    if (n == null) name = "unknown"; else name = n;
    if (age == null) age = 0; else age = a;
}

Java 9 之后可以用 Objects 类的 requireNonNullElse 方法来实现上述功能

public Animal(String n, int a) {
    name = Objects.requireNonNullElse(n, "unknown");
    age = Objects.requireNonNullElse(a, 0);
}

此外还可以直接拒绝传递 null 参数,抛出异常

public Animal(String n, int a) {
    Objects.requireNonNull(n, "null is now allowed");
    name = n;
    age = Objects.requireNonNullElse(a, 0);
}

this

对上述例子中的类增添以下方法

public void grow(int year) {
    age += year;
}
var a = new Animal("Pickle", 2);
a.grow(2);

对于以上 grow 方法实际上执行的是

a.age += year

因此实际上该方法含有两个参数,一个隐式的未写明的参数指定了属于改类的一个对象,在方法内部可以用 this 关键字指明该隐式参数

public void grow(int year) {
    this.age += year;
}

以此可以区分是类的字段还是局部变量

封装的注意事项

所有的 accessor method 方法应避免返回可变对象的引用,例如下面的例子

class Animal {
    private String name;
    private int age;
    private Date birthday;

    // ...

    public Date getBirthday() {
        return birthday; // BAD :(
    }
}
var a = new Animal(...);
birthday = a.getBirthday();

上述的 birthday 变量和对象 a 内部的 field 指向的是同一个对象的引用,因此若对 birthday 变量进行改变也会影响到对象的 field 内部的变量,破坏了封装的原则。如果要返回可更改对象,应该返回一个其对应的拷贝。

public Date getBirthday() {
    return (Date) birthday.clone();
}

基于类的访问权限

定义以下类方法

public boolean equals(Animal other) {
    return name.quals(other.name);
}

以上方法能够正常执行,因为该类内部的方法能够访问所有该类的对象的私有字段。

私有方法

public 修饰符改为 private 创建私有方法,仅能够被类内部的方法调用。

final 字段

对于一些原始数据类型或者是不可变类型的字段,可以加上 final 修饰符,表明该字段必须在所有构造器执行完后有值存在,并且不能再次变更。但如果该字段指向的是可变的对象,那么该对象的值依然可以发生改变,只是意味着该字段不能另外指向另一个变量的引用。

静态字段和静态方法

静态字段

class Animal {
    private static int id = 0;
    private final String name;
}

对于非静态的字段比如上述的 name,每一个该类的对象都拥有自己独立的 name 字段,互不相同,但所有的对象都共享同一个 id 字段,即使目前没有任何该类的对象,该静态字段依然存在,因为该字段属于该类而不是该类的对象。因此修改任意一个对象的静态字段,另外的对象的该静态字段也会发生变化。

静态常量

静态变量并不常见,而静态常量更加常见,比如下面在 Math 类中的一个例子

class Math {
    public static final double PI = 3.1415926;
}

这样便可以直接使用 Math.PI 来访问到这个值。如果不加上 static 修饰符的话,那么必须首先创建该类的对象才能访问该字段,并且每个对象都有该常量的一份拷贝。

在前面提到的,类内部的字段都应该设置为 private,但对于常量即 final 修饰的字段可以设置为 public,因为被声明为 final 的字段不能再被分配新的对象。

静态方法

静态方法是不作用于对象的方法,例如 Math.pow(),该方法不需要用到任何 Math 对象,也就是说,它没有一个指向对象的隐式参数 this。静态方法不能访问到该类的非静态字段,因为静态方法不作用于对象。

class Animal {
    private static int id = 0;

    // ...

    public static int getId() {
        return id;
    }
}

调用该静态方法只需要使用 Animal.getId() 即可,不能够对对象调用静态方法。

使用静态方法的两种情况:

  • 当方法的所有参数皆为显式参数即不需要访问到对象的时候
  • 当方法仅需要访问静态字段的时候

工厂方法

静态方法还常用来构造对象,例如 LocalDate.now()LocalDate.of(),那为何不采用构造方法来构造对象?

  • 构造函数无法自己命名,但有时需要不同的命名来实现构造不同类型的对象。
  • 构造函数无法改变返回的构造对象,但工厂方法却可以返回一个子类的对象。

main 方法

静态方法不需要构造对象便可以使用,因此 main 方法 也应该是静态方法。

此外任何类都可以有一个 main 方法,可以这样进行单元测试。

方法参数

参数传递

Java 的方法传入参数时始终是 call by value,即方法获得了传入参数的值的拷贝,而无法修改参数的值。

public static void triple(double num) {
    num = num * 3;
}
double x = 10.0;
triple(x); // x is still 10.0

因此上述的例子是无效的,因为在方法内部的 num 仅是传入参数变量 x 的拷贝,乘法运算作用在内部的变量 num 上。

然而如果传入的参数不是原始数据类型而是一个对象的引用,如下述例子

public static void growThreeYears(Animal animal) {
    animal.grow(3);
}
Animal a = new Animal("Pickle", 2);
growThreeYears(a);

上述例子是可行的,因为在方法内部 animal 变量被初始化为传入的a 参数的拷贝,而两个变量都指向同一个对象的引用,因此 grow 方法是作用在这个引用的对象上的。

传递值与传递引用

有些语言在传递参数时同时使用了传递值 (call by value) 和传递引用 (call by reference) 两种机制,但 Java 始终是传递值。

public static void swapAnimals(Animal a, Animal b) {
    Animal tmp = a;
    a = b;
    b = tmp;
}

上述交换变量的方法是无效的,因为实际上交换的只是方法内部的 a b 变量的引用,而实际传入的两个参数并未发生改变。

因此方法传参的三个特点如下:

  • 方法不能修改传入的基础数据类型的参数
  • 方法能够调用传入的对象的方法来改变对象的状态
  • 方法不能让传入的对象参数指向另一个新的对象

可变参数

类似于 System.out.printf() 这样的方法可以接收不定个数的参数, 在定义参数时加上 ... 指定该方法可以接收不定个数的参数.

public static void func(Object... args) {
    // Do something
}

实际上该方法接收了一个数组 Object[], 接收到的参数都会传递到该数组里, 如果是原生的数据类型的话会自动包装成对象再传入. 例如下面一个求和函数的例子

public static int sumAll(int... numbers) {
    int result = 0;
    for (int n: numbers) {
        result += n;
    }
    return result;
}

public static main(String[] args) {
    System.out.print(sumAll(1, 2, 3, 4, 5));
}

构造对象

重载

观察下述例子,StringBuilder 的构造器会根据传入参数的不同而构造不同的对象。

var emptyText = new StringBuilder();
var text = new StringBuilder("test");

前文提到一个类可以有多个构造器,即在一个类中有多个同名的方法,比如此处的构造方法,它们功能类似但接收的参数不同,称为方法重载 (Overloading)。方法重载的返回值类型应该相同,但返回的对象可以是同一个父类下的子类。

此外在方法重载时,父类和子类的方法应该有相同的权限级别,即若父类的方法声明为 public,子类重载的方法也应该为 public

默认字段值

当在构造对象时如果没有传入指定参数的值,那么在构建对象时会采用各数据类型的默认值,例如整型默认为 0,布尔型默认为 false,对象引用默认为 null。和局部变量不同,局部变量必须手动初始化值。因此可以声明一个无参数的构造器来指定默认的值。

public Animal() {
    this.name = "";
    this.age = 0;
}

如果某个类没有声明构造器,那么在构造对象时无需指定参数,字段会填充默认值,而如果已经有了构造器且需要参数,那么就不能再构造对象时不指定参数。此时如果想要构造一个字段填充默认值的对象的话可以声明一个空的构造器。

public Animal() {}

除此之外还可以在定义字段的时候便初始化值,这会优先于构造器执行

class Animal {
    private String name = "";
    private int age = 0;
}

字段初始化的值不一定必须是常量,也可以调用方法返回值。

调用另一个构造器

public Animal(String name, int age) {
    this.name = name;
    this.age = age;
}

public Animal(int age) {
    this("unknown", age);
}

如果在构造器内部的第一个语句使用 this(...) 方法的话,可以调用另一个构造方法。

初始化代码块

构造对象时初始化字段值的方法除了有构造方法和在声明字段时初始化值以外,还可以通过初始化代码块实现。

class Animal {
    private static nextId = 0;
    private int id;
    private String name;
    private int age;

    {
        id = nextId;
        nextId++;
    }

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

但这种方法并不常见,不推荐使用。

构造器的执行过程

  1. 如果构造方法的第一句声明调用了另一个构造方法,则首先执行另一个构造器
    1. 所有的字段都填充数据类型的默认值
    2. 按照声明字段的顺序初始化值或执行初始化代码块
  2. 执行构造方法内的代码

静态字段的初始化

直接在声明静态字段时初始化值或者是使用静态代码块来指定值

class Animal {
    private static int nextId = 0;
    private int id;

    static {
        var generator = new Random();
        nextId = generator.nextInt(100);
    }

    {
        id = nextId;
        nextId++;
    }
}

为了解决类名冲突的问题,可以引入包以便于组织代码,通常的包名习惯将域名倒序命名,例如 com.xxxxx.xxx

但值得注意的是,例如 java.utiljava.util.jar 是两个不同的包,两者没有任何继承关系。

声明包

在源码的最上方声明包,同时源码存放的位置也要遵循包的层级路径。如果没有在源码最上方声明包的话,该类会默认在未命名的包中。

package one.tunkshif.test;

class Test {
    // ...
}

导入类

在一个类中可以任意使用自己包内的类或者是其它包内的公开类,例如

java.time.LocalDate now = java.time.LocalDate.now();

但上述方式非常麻烦,因此可以现在源码开头导入类方便使用

import java.time.*; // import all classes from the package
import java.time.LocalDate; // only import the specific class

此外,通配符 * 只能用来导入单个包的所有类,因此导入 java.*.*无效的。

还有避免导入两个不同的包内的相同类。

静态导入

如下例可以导入某个包内的所有静态字段和静态方法不只是导入类,这样可以不加包的前缀使用静态方法或字段。

import static java.lang.System.*;
import static java.lang.System.out;

包的访问权限

使用 publicprivate 修饰符可以声明访问权限,对于没有加权限修饰符的方法,可以被同一个包内的其它类访问。

定义类时的提示

  • 保持数据私有
  • 总是初始化数据
  • 不要在数据中使用太多基础类型
  • 并非所有数据都需要 gettersetter
  • 将功能太多太杂的类剥离开来
  • 类或方法的命名要直观反映其作用
  • 偏向于使用不可变的类

继承 Inheritance

一个类可以继承自另一个类,之间形成子类和父类的关系,子类可以复用父类的字段和方法,同时又可以添加新的字段和方法。

子类

定义子类

class Cat extends Animal {
    // ...
}

使用 extends 关键字来定义子类,所继承自的类称作超类、基类或父类。

复写方法
class Animal {
    private String final name;

    // ...

    public void sleep() {
        msg = name + " is sleeping...";
        System.out.println(msg);
    }
}

class Cat extends Animal {
    // ...

    public void sleep() {
        msg = "Cat " + name + " is sleeping..."; // !won't work
        System.out.println(msg);
    }
}

上述的例子中子类复写了父类的 sleep 方法,但不能实现,因为 name 字段被声明为 private,只有父类 Animal 的对象才能够直接访问到该字段,要在子类当中获取该字段必须改用公开的方法。

public void sleep() {
    name = super.getName();
    msg = "Cat " + name + " is sleeping...";
    System.out.println(msg);
}

使用 super 关键字来表明调用的是父类的方法。

注意 thissuper 两者并不完全类似,this 指向的是对象的引用,然而 super 并不是任何对象的引用

构造方法

class Cat extends Animal {
    private String owner;

    public Cat(String name, int age, String name) {
        super(name, age);
        this.owner = owner;
    }
}

使用 super() 来调用父类的构造方法,子类不能直接访问父类的私有字段,因此必须调用构造器来初始化字段值,super() 语句必须放在子类构造器声明的最上方。

如果子类的构造器没有显式地调用父类构造器,那么会直接调用父类的无参构造器,如果父类没有定义无参构造器,编译器会报错。

此外,我们还可以定义一个新的类继承自 Cat 类,也可以定义多个其它的类继承自 Animal

判断自己的类的继承设计是否合理时,可以简单地用 is-a 的方法,即看子类是否属于父类。

多态

var a = new Cat("Pickle", 2, "TunkShif");
var b = new Animal();
var c = new Animal();

Animal[] animals = {a, b, c};

for (Animal animal: animals) {
    animal.sleep();
}

观察上述例子,可以发现数组 animals 的类型是 Animal,然而变量 a 指向的对象属于 Cat 类,同样可以正常执行,并调用各自类的 sleep 方法。像上述的变量 animal 一样可以指代多个不同类的对象,这叫做多态 (polymorphism)。而在运行时能正确调用不同对象的方法,这叫做动态绑定 (dynamic binding)。

Animal a;
a = new Animal();
a = new Cat(); // also acceptable

声明变量 a 时我们期望的类型为 Animal,但实际上变量 a 也可以是 Cat 类的对象,因为 Java 中的对象变量是多态的,父类类型的变量可以赋值为任意子类类型的对象。

class Cat extends Animal {
    // ...

    public void play() {
        var name = super.getName();
        var msg = "Cat " + name + " is now playing...";
        System.out.println(msg);
    }
}

如果给 Cat 类定义一个特有的新方法,回到上面的数组的例子当中,

a.play(); // is OK
animals[0].play(); // won't work

因为数组定义时的类型为 Animal,而该类并没有一个叫做 play 的方法,在编译器中 animals[0] 仅被视为一个 Animal 类的对象。

并且无法将超类的引用赋给子类类型的变量。

方法调用

对于 x.f(args) 这样一个方法调用语句,在执行时发生以下过程:

  1. 编译器枚举该对象对应的类中所有的同名方法以及其父类中所有的公开的同名方法
  2. 编译器根据提供的参数类型确定实际调用的方法,如果没有符合的方法则报错
  3. 如果调用的方法是 private final static 修饰的或者是构造方法,那么编译器清楚知道该调用哪个方法,这叫做静态绑定 (static binding)。否则所调用的方法取决于隐式参数的类型即所调用方法的变量的类型,那么动态绑定必须在运行时进行。
  4. 当在运行时并使用动态绑定时,虚拟机必须调用对象 x 所引用的恰当的类型的方法。例如假设 x 属于 C 的一个子类 D,且 D 类定义了一个叫 f(String) 的方法,那么实际上便调用此方法,否则便会在父类 C 中搜寻名叫 f 的方法。

如果每次调用方法时都进行上述搜索过程,将会非常耗时,因此虚拟机会预先为每个类生成一张方法表 (method table),列举出所有的 method signatures 和实际调用的方法,当某个方法被调用时,直接在表中查询。

阻止继承

有时我们可能需要防止某个类再被继承,只需要在定义类时加上 final 修饰符。如果只是想要子类无法复写父类的某个方法,同样加上 final 修饰符即可。被声明为 final 的类的所有方法都默认是 final 的。

class final PersianCat extends Cat {
    // ...
}
class Animal {
    // ...

    public final String getName() {
        return name;
    }
}

需要注意的是,声明为 final 的类只有其方法才会被自动声明为 final,而字段不会。

对象转型

在之前的内容中已经接触到了对象转型 (casting),例如下面的例子将浮点型的 x 直接转为了整数型的 i

double x = 3.14;
int i = (int) x;

回到上面讲多态时的数组的例子,

Animal[] animals = {a, b, c}; // a is Cat, b and c are Animal
var myCat = animals[0]; // myCat is Animal
var myRealCat = a; // myRealCat is Cat

Cat myCat = animals[1]; // ERROR
Cat myCat = (Cat) animals[0]; // OK

可以看到虽然 aCat 类型,但 animals[0] 实际上是 Animal 类型,所以 myCat 不能调用 Cat 类特有的方法,需要进行转型后才能调用。也就是说子类的对象可以转型成父类,但父类的对象不能转型成子类。

可以使用 instanceOf 关键字来判定某个变量是否属于某类

var isAnimal = animals[0] instanceOf Animal; // true
var isCat = animals[0] instanceOf Cat; // true

var isRealCat = animals[1] instanceOf Cat; //false

抽象类

假设我们定义了 Employee Student Manager 等不同的类,Manager 可以继承自 Employee 类,而 Employee Student 类都可以再继承自 Person 类,而 Person 这样的一个类可能仅仅包含了姓名等基本的信息,不足以描绘一个具体的对象。所有的对象都是用类来描述的,但反过来并不是所有的类都能描述对象,这样的类就叫做抽象类 (Abstract Class)。

类似地,如果一个方法在父类中不需要具体的实现,它的具体实现由其子类实现,这样的方法叫做抽象方法 (abstract method),用 abstract 关键字定义。

class abstract Animal() {
    // ...

    public abstract void sleep();
}

需要注意的是,如果一个类拥有抽象方法的话,这个类也必须定义为抽象类。抽象类同样可以拥有具体方法 (concrete method)。抽象类不能被实例化。

如果一个类继承了抽象类,子类必须重写父类的抽象方法或者是自身也声明为抽象类,最终必须要有子类实现抽象方法,否则仍然无法实例化。

枚举类 Enumeration Class

一个最简单的枚举类定义如下, 枚举类不能被实例化, 其中的每一个枚举项目实际上都是一个 final 修饰的静态常量字段,

public enum Size {
    SMALL,
    MEDIUM,
    LARGE,
    EXTRA_LARGE
}

public static void main(String[] args) {
    Size s = Size.SMALL;
}

枚举类同样能够具有字段和构造方法, 其构造方法必须是私有的, 其构造方法只能在枚举常量初始化时调用, 例如下面的例子

public enum Color {
    RED, BLUE, WHITE;

    private Color() {
        System.out.println(this.toString() + " called!");
    }

    public void colorInfo() {
        System.out.println("Colors!");
    }
}

public static void main(String[] args) {
    Color c1 = Color.RED;
    c1.colorInfo();
}

上述例子输出的内容如下

RED called!
BLUE called!
WHITE called!
Colors!

可以看到, 我们只使用到了 Color.RED, 在 c1 创建的时候, 构造方法执行, 所有颜色的枚举量都输出了一遍, 我们同样可以为枚举类建立字段, 单独构造每一个枚举项目

public enum Color {
    RED("R"), WHITE("W"), BLUE("B");

    private final String abbr;

    private Color(String abbr) {
        this.abbr = abbr;
    }

    public String getAbbr() {
        return this.abbr;
    }
}

当我们第一次使用枚举类, 即创建枚举对象的时候, 会自动将其中的各个枚举项目进行初始化.

所有的枚举类都是 Enum 的子类, 继承了许多的方法. 例如将枚举对象的名称转为字符串, 从字符串构建枚举对象, 快速生成所有枚举项目的列表, 返回枚举项目的所在编号等.

Color.RED.toString(); // "SMALL"
Color color = Enum.valueOf(Color.class, "RED"); // Color.RED
Color[] colors = Color.values();
Color.RED.ordinal(); // 0

接口 Interface

前文在提到抽象类的时候, 抽象类中的抽象方法本身不提供实现, 只是定义了接口的规范, 如果一个抽象类不需要实例字段, 只是一堆抽象方法的集合, 我们可以将其定义为接口. 这里的接口指的是 Java 中 interface, 而不同于一般广义的编程接口.

例如下面定义的一个接口, 所有实现( implement )了该接口的类都需要有一个 compareTo() 方法, 接收另一个对象作参数, 返回一个整数值.

public interface Comparable {
    int compareTo(Object other);
}

接口不能被实例化, 因此不能使用 new 操作符来创建接口对象, 但可以将变量类型声明为接口类型, 其指向的对象必须实现了该接口.

接口继承

接口同样可以继承自另一个接口

public interface Movable {
    void move(int x, int y);
}

public interfave Powered extends Movable {
    double milesPerGallon();
}

接口常量

接口无法具有实例化的字段, 但可以在接口内定义静态常量. 接口内定义的常量默认都是 public static final 修饰的, 可以省略不加

public interface Movable {
    double SPEED_LIMIT = 120;
}

多继承

Java 中一个类只能有一个超类, 即只能继承自一个类, 因此 Java 是不支持多继承的. 但一个类可以实现多个接口, 因此一个类可以实现多个接口, 继承不同接口的方法.

静态方法和私有方法

从 Java 8 开始, 接口中允许定义静态方法和私有方法.

默认方法

我们可以为接口内的方法增设一个默认的实现, 使用 default 关键字指明默认方法

public interface Comparable {
    defautl int compareTo(Object other) { return 0; }
}

实现了该接口的类不需要实现接口中的默认方法.

方法重名冲突

如果类中本身定义的实体方法和其所继承的接口中也定义了同名的默认方法, 会优先调用类中的方法.

反射 Reflection

Java 的反射机制允许我们在运行时获取一个到一个对象的信息.

Class

获取 Class 类实例

Java 程序在运行的时候, 总是保留着每个对象所属的类的信息, 而这些信息存储在一个名叫 Class 的类里面, 调用 Object 类的 getClass() 方法, 可以得到一个 Class 类的实例, 通过调用其 getName() 等方法, 可以获取到类的信息.

Animal a = new Animal("Pickle", 3);
Class cls = a.getClass();
String className = cls.getName();

如果我们知道一个类的完整名称, 也可以使用 Class.forName() 方法来获得该类的 Class 实例

String className = "java.lang.String";
Class cls = Class.forName(className);

获取 Class 实例的第三种方法是, 使用类的一个特殊的静态变量 Object.class

Class cls = String.class

每一个类都只有一个相同的 Class 类实例, 因此通过以上三种方法获得的相同类的 Class 实例是相同的, 可以直接使用 == 运算符进行比较. 例如对于下面的例子, 如果 objAnimal 类的一个实例, 那么会返回 true. 这与 instanceof 的区别是, 如果 objAnimal 子类的实例, 使用 getClass() 方法判定为 false, 而使用 instanceof 判定为 true.

if (obj.getClass() == Animal.class) {
    // ...
}

需要注意的是, 数组(例如 String[] )也是一类特殊的类, 类名叫做 [Ljava.lang.String. JVM 为各种基本类型也创建了相应的 Class 实例, 使用 int.class 来获取.

构建新的实例

当我们获取到某个类的 Class 实例后, 便可以通过此构建新的实例. 如果按照以下方法创建实例的话, 若该类没有无参构造器会抛出异常.

Class cls = Class.forName("java.util.Random");
Object obj = cls.getConstructor().newInstance();