02—java面向对象

。。。

目录

一、类与对象

1.1、类与对象的概念

1.1.1、概念

1.1.2、面向对象的特征

1.2、类的定义与使用

1.2.1、类的定义—创建类

1.1.2、类的使用—创建对象

 

1.3、源文件声明规则

二、方法

2.1、方法的定义与使用

2.1.1、定义

2.2.2、private方法

2.2.3、this变量

2.2.4、方法的调用

 

2.2、方法的参数

2.2.1、方法参数

2.2.2、可变参数

2.2.3、参数绑定

2.3、构造方法

2.3.1、默认构造方法

2.3.2、多构造方法

2.4、方法重载

三、继承

3.1 protected

3.2 super

3.3 向上转型:把一个子类型安全地转为更加抽象的父类型

3.3 向下转型:父类类型强制转为子类类型

 

3.4 区分继承和组合

 

四、多态

4.1 覆写(Override)

多态

覆写Object方法

调用super

final


一、类与对象

1.1、类与对象的概念

1.1.1、概念

class:是一种对象模版,它定义了如何创建实例,因此,class本身就是一种数据类型

instance:是对象实例,instance是根据class创建的实例,可以创建多个instance,每个instance类型相同,但各自属性可能不相同

1.1.2、面向对象的特征

面向对象的基本特征是封装、继承、多态

封装:具有两层含义: (1)把对象的全部属性和基于数据的方法封装在一起,形成为一个整体对象。(2)隐蔽信息,即尽可能的隐蔽对象的内部细节,对外形成一个屏障,只保留有限的接口使之与外界发生联系。这样可以极大的保证内部数据的安全性,可以避免外部环境对内部数据的侵扰和破坏。

继承:是指一个类拥有另一个类的数据和操作。被继承的类称为父类,继承了父类的数据和操作的类称为子类。通过类的继承,可以实现程序代码的重复使用,使得程序结构清晰,降低编程和维护的工作量。
Java只支持单重继承,不支持多重继承,即一个子类只能有一个父类,但一个父类可以有多个子类,这样使得Java的继承层次形成了树状结构。单重继承使得程序结构变得简单,对于一些复杂的现实问题,当用单重继承不能解决时,Java可以通过实现接口的方式来弥补单重继承的不足,以达到解决复杂问题的目的。

多态:是指程序的多种表现形式,是面向对象程序设计代码重用的一个重要机制。Java中的多态分为方法多态变量多态,方法多态分为方法重载方法覆盖

1.2、类的定义与使用

1.2.1、类的定义—创建类

创建一个类,例如,给这个类命名为Person,就是定义一个class

class Person {
    public String name;
    public int age;
}

一个class可以包含多个字段(field),字段用来描述一个类的特征。

上面的Person类,我们定义了两个字段,一个是String类型的字段,命名为name,一个是int类型的字段,命名为age

因此,通过class,把一组数据汇集到一个对象上,实现了数据封装。

public是用来修饰字段的,它表示这个字段可以被外部访问。

1.1.2、类的使用—创建对象

对象是根据类创建的。在Java中,使用关键字new来创建一个新的对象。创建对象需要以下三步:

  • 声明:声明一个对象,包括对象名称和对象类型。
  • 实例化:使用关键字new来创建一个对象。
  • 初始化:使用new创建对象时,会调用构造方法初始化对象。

定义了class,只是定义了对象模版,而要根据对象模版创建出真正的对象实例,必须用new操作符。

new操作符,可以创建一个实例,然后,我们需要定义一个引用类型的变量来指向这个实例:

Person ming = new Person();

上述代码创建了一个Person类型的实例,并通过变量ming指向它。

注意区分Person ming是定义Person类型的变量ming,而new Person()是创建Person实例。

访问实例变量和方法

有了指向这个实例的变量,我们就可以通过这个变量来操作实例。访问实例变量可以用变量.字段,例如:

ming.name = "Xiao Ming"; // 对字段name赋值
ming.age = 12; // 对字段age赋值
System.out.println(ming.name); // 访问字段name

Person hong = new Person();
hong.name = "Xiao Hong";
hong.age = 15;

 

1.3、源文件声明规则

当在一个源文件中定义多个类,并且还有import语句和package语句时,要特别注意这些规则。

  • 一个源文件中只能有一个public类
  • 一个源文件可以有多个非public类
  • 源文件的名称应该和public类的类名保持一致。例如:源文件中public类的类名是Employee,那么源文件应该命名为Employee.java。
  • 如果一个类定义在某个包中,那么package语句应该在源文件的首行。
  • 如果源文件包含import语句,那么应该放在package语句和类定义之间。如果没有package语句,那么import语句应该在源文件中最前面。
  • import语句和package语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。

类有若干种访问级别,并且类也分不同的类型:抽象类和final类等。这些将在访问控制章节介绍。

除了上面提到的几种类型,Java还有一些特殊的类,如:内部类、匿名类。

二、方法

一个class可以包含多个field,但是,直接把fieldpublic暴露给外部可能会破坏封装性。为了避免外部代码直接去访问field,我们可以用private修饰field,拒绝外部访问。

fieldpublic改成private,外部代码不能访问这些field,需要使用方法(method来让外部代码可以间接修改field,外部代码可以调用方法来间接修改private字段,在方法内部,我们就有机会检查参数对不对

  • 方法可以让外部代码安全地访问实例字段;

  • 方法是一组执行语句,并且可以执行任意逻辑;

  • 方法内部遇到return时返回,void表示不返回任何值(注意和返回null不同);

  • 外部代码通过public方法操作实例,内部代码可以调用private方法;

  • 理解方法的参数绑定。

2.1、方法的定义与使用

2.1.1、定义

从上面的代码可以看出,定义方法的语法是:

修饰符 方法返回类型 方法名(方法参数列表) {
    若干方法语句;
    return 方法返回值;
}

方法返回值通过return语句实现,如果没有返回值,返回类型设置为void,可以省略return

class Person {
    private String name;
    public int age;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
    if (name == null || name.isBlank()) {
        throw new IllegalArgumentException("invalid name");
    }
    this.name = name.strip(); // 去掉首尾空格
}

2.2.2、private方法

public方法,自然就有private方法。和private字段一样,private方法不允许外部调用,那我们定义private方法有什么用?

定义private方法的理由,是内部方法是可以调用private方法的。例如:

class Person {
    private String name;
    private int birth;

    public void setBirth(int birth) {
        this.birth = birth;
    }

    public int getAge() {
        return calcAge(2019); // ===调用private方法
    }

    // =====private方法:
    private int calcAge(int currentYear) {
        return currentYear - this.birth;
    }
}

观察上述代码,calcAge()是一个private方法,外部代码无法调用,但是,内部方法getAge()可以调用它。

此外,我们还注意到,这个Person类只定义了birth字段,没有定义age字段,获取age时,通过方法getAge()返回的是一个实时计算的值,并非存储在某个字段的值。这说明方法可以封装一个类的对外接口,调用方不需要知道也不关心Person实例在内部到底有没有age字段。

2.2.3、this变量

在方法内部,可以使用一个隐含的变量this,它始终指向当前实例。因此,通过this.field就可以访问当前实例的字段。

如果没有命名冲突,可以省略this。例如:

class Person {
    private String name;

    public String getName() {
        return name; // 相当于this.name
    }
}

但是,如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this

class Person {
    private String name;

    public void setName(String name) {
        this.name = name; // 前面的this不可少,少了就变成局部变量name了
    }
}

2.2.4、方法的调用

public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.setBirth(2008);
        System.out.println(ming.getAge());
    }
}

 

2.2、方法的参数

2.2.1、方法参数

方法可以包含0个任意个参数。方法参数用于接收传递给方法的变量值。

调用方法时,必须严格按照参数的定义一一传递。例如:

class Person {
    ...
    public void setNameAndAge(String name, int age) {
        ...
    }
}

调用这个setNameAndAge()方法时,必须有两个参数,且第一个参数必须为String,第二个参数必须为int

Person ming = new Person();
ming.setNameAndAge("Xiao Ming"); // 编译错误:参数个数不对
ming.setNameAndAge(12, "Xiao Ming"); // 编译错误:参数类型不对

2.2.2、可变参数

可变参数用类型...定义,可变参数相当于数组类型

class Group {
    private String[] names;//===

    public void setNames(String... names)//====
   {
        this.names = names;
    }
}

上面的setNames()就定义了一个可变参数。调用时,可以这么写:

Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 传入3个String
g.setNames("Xiao Ming", "Xiao Hong"); // 传入2个String
g.setNames("Xiao Ming"); // 传入1个String
g.setNames(); // 传入0个String

完全可以把可变参数改写为String[]类型:

class Group {
    private String[] names;//====

    public void setNames(String[] names)//=====
    {
        this.names = names;
    }
}

但是,调用方需要自己先构造String[],比较麻烦。例如:

Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]

另一个问题是,调用方可以传入null

Group g = new Group();
g.setNames(null);

而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null

2.2.3、参数绑定

调用方 把参数传递给 实例方法时,调用时传递的值会按参数位置一一绑定。

基本类型参数:八大基本类型    引用类型参数:Sring等

基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响

引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。

2.3、构造方法

构造方法:创建对象实例时,就把内部字段,全部初始化为合适的值

构造方法的名称,就是类名

构造方法的参数,没有限制,在方法内部,也可以编写任意语句。

但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符


class Person {
    private String name;
    private int age;

    public Person(String name, int age) {//====
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person("Xiao Ming", 15);//===
        System.out.println(p.getName());
        System.out.println(p.getAge());
    }
}

2.3.1、默认构造方法

是不是任何class都有构造方法?是的。那前面我们并没有为Person类编写构造方法,为什么可以调用new Person()

如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,类似这样:

class Person {
    public Person() {
    }
}

注意:若,自定义了一个构造方法,则编译器就不再自动创建默认构造方法

没有在构造方法中初始化字段时,

                 引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false

class Person {
    private String name; // 默认初始化为null
    private int age; // 默认初始化为0

    public Person() {
    }
}

也可以对字段直接进行初始化

class Person {
    private String name = "Unamed";
    private int age = 10;
}

那么问题来了:既对字段进行初始化,又在构造方法中对字段进行初始化:

class Person {
    private String name = "Unamed";
    private int age = 10;

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

当我们创建对象的时候,new Person("Xiao Ming", 12)得到的对象实例,字段的初始值是啥?

在Java中,创建对象实例的时候,按照如下顺序进行初始化:

  1. 初始化字段

  2. 执行构造方法的代码进行初始化

因此,构造方法的代码由于后运行,所以,new Person("Xiao Ming", 12)的字段值最终由构造方法的代码确定。

2.3.2、多构造方法

如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来:

可以定义多个构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分

class Person {
    private String name;
    private int age;

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

    public Person(String name) {
        this.name = name;
        this.age = 12;
    }

    public Person() {
    }
}

如果调用new Person("Xiao Ming", 20);,会自动匹配到构造方法public Person(String, int)

如果调用new Person("Xiao Ming");,会自动匹配到构造方法public Person(String)

如果调用new Person();,会自动匹配到构造方法public Person()

一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)

class Person {
    private String name;
    private int age;

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

    public Person(String name) {
        this(name, 18); // 调用另一个构造方法Person(String, int)
    }

    public Person() {
        this("Unnamed"); // 调用另一个构造方法Person(String)
    }
}

2.4、方法重载

在一个类中,我们可以定义多个方法。

如果有一系列方法,它们的功能都是类似的,只有参数有所不同,

那么,可以把这一组方法名做成同名方法

这种方法名相同,但各自的参数不同,称为方法重载(Overload)。

例如,在Hello类中,定义多个hello()方法:

class Hello {
    public void hello() {
        System.out.println("Hello, world!");
    }

    public void hello(String name) {
        System.out.println("Hello, " + name + "!");
    }

    public void hello(String name, int age) {
        if (age < 18) {
            System.out.println("Hi, " + name + "!");
        } else {
            System.out.println("Hello, " + name + "!");
        }
    }
}

注意:方法重载的返回值类型通常都是相同的。

方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单

三、继承

  • 继承是面向对象编程的一种强大的代码复用方式;

  • Java只允许单继承,所有类最终的根类是Object

  • protected允许子类访问父类的字段和方法;

  • 子类的构造方法可以通过super()调用父类的构造方法;

  • 可以安全地向上转型为更抽象的类型;

  • 可以强制向下转型,最好借助instanceof判断;

  • 子类和父类的关系是is,has关系不能用继承。

继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。

当我们让StudentPerson继承时,Student就获得了Person的所有功能,我们只需要为Student编写新增的功能。

Java使用extends关键字来实现继承:

class Person {
    private String name;
    private int age;

    public String getName() {...}
    public void setName(String name) {...}
    public int getAge() {...}
    public void setAge(int age) {...}
}

class Student extends Person {
    // 不要重复name和age字段/方法,
    // 只需要定义新增score字段/方法:
    private int score;

    public int getScore() { … }
    public void setScore(int score) { … }
}

在OOP(面向对象编程)的术语中,

Person称为:超类(super class),父类(parent class),基类(base class),

Student称为:子类(subclass),扩展类(extended class)。

注意到,我们在定义Person的时候,没有写extends。在Java中,没有明确写extends的类,编译器会自动加上extends Object。所以,任何类,除了Object,都会继承自某个类。

Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。

  • 3.1 protected

    • 继承有个特点,就是子类无法访问父类private字段或者private方法
    • private改为protected。用protected修饰的字段可以被子类访问
    • class Person {
          protected String name;
          protected int age;
      }
      
      class Student extends Person {
          public String hello() {
              return "Hello, " + name; // OK!
          }
      }
  • 3.2 super

    • super关键字表示父类(超类)。
    • 子类引用父类的字段时,可以用super.fieldName
    • class Student extends Person {
          public String hello() {
              return "Hello, " + super.name;
          }
      }

       这里使用super.name,或者this.name,或者name,效果都是一样的。编译器会自动定位到父类的name字段。

在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法

如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();,所以,Student类的构造方法实际上是这样:

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(); // 自动调用父类的构造方法
        this.score = score;
    }
}

但是,Person类并没有无参数的构造方法,因此,编译失败。

解决方法  :调用Person类存在的某个构造方法。例如:

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(name, age); // 调用父类的构造方法Person(String, int)
        this.score = score;
    }
}

这样就可以正常编译了!

因此我们得出结论:

如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法

这里还顺带引出了另一个问题:

即   子类不会继承任何父类的构造方法子类默认的构造方法是编译器自动生成的,不是继承的。

3.3 向上转型:把一个子类型安全地转为更加抽象的父类型

如果一个引用变量的类型是Student,那么它可以指向一个Student类型的实例:

Student s = new Student();

如果一个引用类型的变量是Person,那么它可以指向一个Person类型的实例:

Person p = new Person();

现在问题来了:如果Student是从Person继承下来的,

那么,一个引用类型为Person的变量,能否指向Student类型的实例

Person p = new Student(); // ???

测试一下就可以发现,这种指向是允许的!

这是因为Student继承自Person,因此,它拥有Person的全部功能。

Person类型的变量,如果指向Student类型的实例,对它进行操作,是没有问题的!

这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)

向上转型实际上是:把一个子类型安全地变为更加抽象的父类型

Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok

注意到继承树是Student > Person > Object,所以,可以把Student类型转型为Person,或者更高层次的Object

3.3 向下转型:父类类型强制转为子类类型

和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。例如:

Person p1 = new Student(); // Person类型变量p1  实际指向Student实例
Person p2 = new Person();  // Person类型变量p2  实际指向Person实例
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

如果测试上面的代码,可以发现:

在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,

p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。

因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException

为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型:

Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

Student n = null;
System.out.println(n instanceof Student); // false

instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false

利用instanceof,在向下转型前可以先判断:

Person p = new Student();
if (p instanceof Student) {
    // 只有判断成功才会向下转型:
    Student s = (Student) p; // 一定会成功
}

 

3.4 区分继承和组合

在使用继承时,我们要注意逻辑一致性。

考察下面的Book类:

class Book {
    protected String name;
    public String getName() {...}
    public void setName(String name) {...}
}

这个Book类也有name字段,那么,我们能不能让Student继承自Book呢?

class Student extends Book {
    protected int score;
}

显然,从逻辑上讲,这是不合理的,Student不应该从Book继承,而应该从Person继承

究其原因,是因为StudentPerson的一种,它们是is关系,而Student并不是Book。实际上StudentBook的关系是has关系

具有has关系不应该使用继承,而是使用组合,即Student可以持有一个Book实例

class Student extends Person {
    protected Book book;//======
    protected int score;
}

因此,继承是is关系,组合是has关系。

 

四、多态

4.1 覆写(Override)

在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)

例如,在Person类中,我们定义了run()方法:

class Person {
    public void run() {
        System.out.println("Person.run");
    }
}

在子类Student中,覆写这个run()方法:

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}
  • OverrideOverload不同的是:
    • 如果方法签名如果不同,就是Overload,Overload方法是一个新方法;
    • 如果方法签名相同,并且返回值也相同,就是Override

 注意:方法名相同,方法参数相同,但方法返回值不同也是不同的方法。在Java程序中,出现这种情况,编译器会报错。

class Person {
    public void run() { … }
}

class Student extends Person {
    
    public void run(String s) { … } // 不是Override,因为参数不同:
    
    public int run() { … }   // 不是Override,因为返回值不同:
}

@Override不是必需的,加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。

在上一节中,我们已经知道,引用变量的声明类型可能与其实际类型不符,例如:

Person p = new Student();

一个实际类型为Student,引用类型为Person的变量,调用其run()方法,调用的是Person还是Studentrun()方法?

  实际上调用的方法是Studentrun()方法。因此可得出结论:

Java的实例方法调用是基于运行时的实际类型的动态调用,而变量的声明类型

这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂:Polymorphic。

4.2、多态 Polymorphic

多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。例如:

Person p = new Student();
p.run(); // 无法确定运行时究竟调用哪个run()方法

有童鞋会问,从上面的代码一看就明白,肯定调用的是Studentrun()方法啊。

但是,假设我们编写这样一个方法:

public void runTwice(Person p) {
    p.run();
    p.run();
}

它传入的参数类型是Person,我们是无法知道传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类,因此,也无法确定调用的是不是Person类定义的run()方法。

所以,多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。这种不确定性的方法调用,究竟有什么作用?

我们还是来举栗子。

假设我们定义一种收入,需要给它报税,那么先定义一个Income类:

class Income {
    protected double income;
    public double getTax() {
        return income * 0.1; // 税率10%
    }
}

对于工资收入,可以减去一个基数,那么我们可以从Income派生出SalaryIncome,并覆写getTax()

class Salary extends Income {
    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

如果你享受国务院特殊津贴,那么按照规定,可以全部免税:

class StateCouncilSpecialAllowance extends Income {
    @Override
    public double getTax() {
        return 0;
    }
}

现在,我们要编写一个报税的财务软件,对于一个人的所有收入进行报税,可以这么写:

public double totalTax(Income... incomes) {
    double total = 0;
    for (Income income: incomes) {
        total = total + income.getTax();
    }
    return total;
}

观察totalTax()方法:利用多态,totalTax()方法只需要和Income打交道,它完全不需要知道SalaryStateCouncilSpecialAllowance的存在,就可以正确计算出总的税。如果我们要新增一种稿费收入,只需要从Income派生,然后正确覆写getTax()方法就可以。把新的类型传入totalTax(),不需要修改任何代码。

可见,多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。

覆写Object方法

因为所有的class最终都继承自Object,而Object定义了几个重要的方法:

  • toString():把instance输出为String
  • equals():判断两个instance是否逻辑相等;
  • hashCode():计算一个instance的哈希值。

在必要的情况下,我们可以覆写Object的这几个方法。例如:

class Person {
    ...
    // 显示更有意义的字符串:
    @Override
    public String toString() {
        return "Person:name=" + name;
    }

    // 比较是否相等:
    @Override
    public boolean equals(Object o) {
        // 当且仅当o为Person类型:
        if (o instanceof Person) {
            Person p = (Person) o;
            // 并且name字段相同时,返回true:
            return this.name.equals(p.name);
        }
        return false;
    }

    // 计算hash:
    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}

调用super

在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。例如:

class Person {
    protected String name;
    public String hello() {
        return "Hello, " + name;
    }
}

Student extends Person {
    @Override
    public String hello() {
        // 调用父类的hello()方法:
        return super.hello() + "!";
    }
}

final

继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override

class Person {
    protected String name;
    public final String hello() {
        return "Hello, " + name;
    }
}

Student extends Person {
    // compile error: 不允许覆写
    @Override
    public String hello() {
    }
}

如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承:

final class Person {
    protected String name;
}

// compile error: 不允许继承自Person
Student extends Person {
}

对于一个类的实例字段,同样可以用final修饰。用final修饰的字段在初始化后不能被修改。例如:

class Person {
    public final String name = "Unamed";
}

final字段重新赋值会报错:

Person p = new Person();
p.name = "New Name"; // compile error!

可以在构造方法中初始化final字段:

class Person {
    public final String name;
    public Person(String name) {
        this.name = name;
    }
}

这种方法更为常用,因为可以保证实例一旦创建,其final字段就不可修改。

发布了70 篇原创文章 · 获赞 25 · 访问量 6396

猜你喜欢

转载自blog.csdn.net/TU_JCN/article/details/102816243