(笔记)Java面向对象

207 阅读18分钟

面向对象编程 - 廖雪峰

Java是一种面向对象的编程语言。面向对象编程,英文是Object-Oriented Programming,简称OOP。

面向过程编程,是把模型分解成一步一步的过程。

面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。


本章学习

面向对象的基本概念,包括:

  • 实例
  • 方法

面向对象的实现方式,包括:

  • 继承
  • 多态

Java语言本身提供的机制,包括:

  • package
  • classpath
  • jar

以及 Java标准库提供的核心类,包括:

  • 字符串
  • 包装类型
  • JavaBean
  • 枚举
  • 常用工具类

1 基础概念

1.1 class和instance

在OOP中,classinstance是“模版”和“实例”的关系;

定义class就是定义了一种数据类型,对应的instance是这种数据类型的实例;

1.1.1 定义class

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

1.1.2 创建实例

Person ming = new Person();

1.1.3 操作实例

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

但是,直接操作field,容易造成逻辑混乱。

1.2 方法

1.2.0 简介

  • 方法可以让外部代码安全地访问实例字段;
    • 方法是一组执行语句,并且可以执行任意逻辑;
    • 方法内部遇到return时返回,void表示不返回任何值(注意和返回null不同);
  • 外部代码通过public方法操作实例,内部代码可以调用private方法
  • 理解方法的参数绑定

1.2.1 private字段

为了避免外部代码直接去访问field,我们可以用private修饰field,拒绝外部访问:

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

1.2.2 方法

此时,我们需要使用方法method)来让外部代码可以间接修改field

public class Main {
    public static void main(String[] args) {
        Person ming = new Person(); // 创建Person类的一个实例ming
        ming.setName("Xiao Ming"); // 设置name
        ming.setAge(12); // 设置age
        System.out.println(ming.getName() + ", " + ming.getAge()); // 获取name和age,并输出
    }
}

class Person { // 定义Person类
    private String name;
    private int age;

    public String getName() { // 获取name
        return this.name; // 【√】
    }

    public void setName(String name) { // 设置name
        if (name == null || name.isBlank()) { // 不允许传入null和空字符串
            throw new IllegalArgumentException("invalid name");
        }
        this.name = name.strip(); // 去掉首尾空格
    }

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

    public void setAge(int age) { // 设置age
        if (age < 0 || age > 100) { // 判断输入是否合理。若不合理抛出异常
            throw new IllegalArgumentException("invalid age value"); // 【√】
        }
        this.age = age;
    }

}

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

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

如果没有返回值,返回类型设置为void,可以省略return

1.2.3 private方法

public方法,自然就有private方法。

private方法不允许外部调用,但内部方法是可以调用private方法的。

1.2.4 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了
    }
}

1.2.5 方法参数

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

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

1.3 构造方法

1.3.0 简介

实例在创建时通过**new操作符会调用其对应的构造方法**,构造方法用于初始化实例;

没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法;

可以定义多个构造方法,编译器根据参数自动判断;

可以在一个构造方法内部调用另一个构造方法,便于代码复用。

1.3.1 构造方法

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

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());
    }
}

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;
    }
}

构造方法很特殊

  • 构造方法的名称就是类名
  • 构造方法的参数没有限制,在方法内部,也可以编写任意语句
  • 但是,和普通方法相比
    • 构造方法没有返回值(也没有void
    • 调用构造方法,必须用new操作符

1.3.2 默认构造方法

任何class都有构造方法

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

class Person {
    public Person() {
    }
}

要特别注意的是,如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法

1.3.3 多个构造方法

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

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

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Xiao Ming", 15); // 既可以调用带2个参数的构造方法
        Person p2 = new Person("Xiao Ming"); // 既可以调用带1个参数的构造方法
        Person p3 = new Person(); // 也可以调用无参数的构造方法
        // 输出
        System.out.println(p1.getName() + " ," + p1.getAge()); // Xiao Ming ,15
        System.out.println(p2.getName() + " ," + p2.getAge()); // Xiao Ming ,12
        System.out.println(p3.getName() + " ," + p3.getAge()); // null ,0
    }
}

class Person {
    private String name;
    private int age;

    public Person() { // 无参数的构造方法
        // 此时,默认String类型的初值是null,int类型的初值是0
    }

    public Person(String name) { // 带1个参数的构造方法
        this.name = name;
        this.age = 12; // 注意这里要有一个默认值
    }

    public Person(String name, int age) { // 带2个参数的构造方法
        this.name = name;
        this.age = age;
    }

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

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

1.4 方法重载

方法重载是指多个方法的方法名相同,但各自的参数不同;

重载方法应该完成类似的功能,参考StringindexOf()

重载方法返回值类型应该相同。

举个例子,StringindexOf()

public class Main {
    public static void main(String[] args) {
        String s = "Test string";
        int n1 = s.indexOf('t');    // 根据字符的Unicode码查找
        int n2 = s.indexOf('t', 4); // 根据字符查找,但指定起始位置
        int n3 = s.indexOf("st");    // 根据字符串查找
        int n4 = s.indexOf("st", 4); // 根据字符串查找,但指定起始位置
        System.out.println(n1); // 3,即Test的t
        System.out.println(n2); // 6,即string的t
        System.out.println(n3); // 2,即Test的st
        System.out.println(n4); // 5,即string的st
    }
}

2 面向对象的实现方式

2.1 继承

2.1.0 简介

  • 继承是面向对象编程的一种强大的代码复用方式;
    • Java只允许单继承,所有类最终的根类是Object
  • protected允许子类访问父类的字段和方法;
    • 子类的构造方法可以通过super() 调用父类的构造方法;
  • 转型
    • 可以安全地向上转型为更抽象的类型;
    • 可以强制向下转型,最好借助instanceof判断;
  • 子类和父类的关系是is,has关系不能用继承。

2.1.1 extends关键字

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

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

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) { … }
}

注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段

2.1.2 继承树

注意到我们在定义Person的时候,没有写extends。在Java中,没有明确写extends的类,编译器会自动加上extends ObjectObject根类

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

下图是PersonStudent继承树

image-20200930210638056

2.1.3 protected

继承有个特点,就是子类无法访问父类的private字段或者private方法。例如,Student类就无法访问Person类的nameage字段。

为了让子类可以访问父类的字段,我们需要把private改为protected

public class Main {
    public static void main(String[] args) {
        Student ming = new Student(); // 创建实例
        System.out.println(ming.hello()); // 输出
    }
}

class Person {
    protected String name = "Alice"; // 改为protect,并给定一个默认值
    protected int age;
}

class Student extends Person { // 继承
    public String hello() {
        return "Hello, " + this.name; // Hello, Alice
    }
}

因此,protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问

2.1.4 super

super关键字表示父类(超类)。


子类引用父类的字段时,可以用super.fieldName。例如:

class Student extends Person {
    public String hello() {
        return "Hello, " + super.name;
    }
}

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


但是,在某些时候,就必须使用super,如调用父类的构造函数

在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();

为了避免错误,我们要自己调用父类Person的构造方法Person(String, int)

package Main;

public class Main {
    public static void main(String[] args) {
        Student ming = new Student("xiao ming", 18, 100); // 调用构造函数进行初始化
        System.out.println(ming.getName()+ ", " +ming.getAge() + ", " +ming.getScore()); // xiao ming, 18, 100
    }
}

// 父类
class Person {
    protected String name;
    protected int age;

    public Person(String name, int age) { // 构造函数
        this.name = name;
        this.age = age;
    }

    protected String getName() {
        return this.name;
    }
    protected int getAge() {
        return this.age;
    }
}

// 子类
class Student extends Person {
    protected int score;

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

    protected int getScore() {
        return this.score;
    }
}

2.1.5 转型

考虑继承树:Student > Person > Object

Student s = new Student(); // 一般情况下
Person p = new Person();

Person p1 = new Student(); // ok,因为Person是Student的父类,向上转型(upcasting)是安全的
Student s1 = (Student) p1; // ok,因为Person类型p1实际指向Student实例

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

error: 因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。

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

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

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

2.1.6 is关系、has关系

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

class Student extends Person { // Student类型继承Person类型(继承是is的关系)
    protected Book book; // Student拥有一个Book实例(组合是has的关系)
    protected int score;
}

2.2 多态

2.2.0 简介

  • 子类可以覆写父类的方法(Override),覆写在子类中改变了父类方法的行为;
  • Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态
  • final修饰符有多种作用:
    • final修饰的方法可以阻止被覆写
    • final修饰的class可以阻止被继承
    • final修饰的field必须在创建对象时初始化,随后不可修改。

2.2.1 覆写

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

覆写要求:

  • 方法名相同
  • 方法参数相同
  • 方法返回值相同

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

public class Main {
    public static void main(String[] args) {
    }
}

class Person {
    public void run() {}
}

public class Student extends Person {
    @Override // Compile error!
    public void run(String s) {} // 需要删掉传入的参数String s
}

2.2.2 多态

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

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

public class Main {
    public static void main(String[] args) {
        // 给一个有普通收入、工资收入和享受国务院特殊津贴的小伙伴算税:
        Income[] incomes = new Income[] { // 创建Income类型的数组,里面有不同的子类的实例
                new Income(3000),
                new Salary(7500),
                new StateCouncilSpecialAllowance(15000)
        };
        System.out.println(totalTax(incomes)); // 输出
    }

    // 计算总税收
    public static double totalTax(Income... incomes) {
        double total = 0;
        for (Income income: incomes) {
            total = total + income.getTax();
        }
        return total;
    }
}

// 父类:普通收入及税收
class Income {
    protected double income;

    public Income(double income) {
        this.income = income;
    }

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

// 子类1:工资收入及税收
class Salary extends Income {
    public Salary(double income) {
        super(income);
    }

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

// 子类2:国务院特殊津贴及税收
class StateCouncilSpecialAllowance extends Income {
    public StateCouncilSpecialAllowance(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        return 0;
    }
}

观察totalTax()方法:利用多态totalTax()方法只需要和父类Income打交道,它完全不需要知道子类SalaryStateCouncilSpecialAllowance的存在,就可以正确计算出总的税。

如果我们要新增一种稿费收入,只需要从Income派生,然后正确覆写getTax()方法就可以。把新的类型传入totalTax(),不需要修改任何代码。

2.2.3 覆写Object方法

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

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

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

package Main;

public class Main {
    public static void main(String[]args){
        Person ming = new Person("xiao ming");
        Person hong = new Person("xiao hong");
        System.out.println(ming.toString()); // Person:name = xiao ming
        System.out.println(ming.equals(hong)); // false
        System.out.println(ming.hashCode()); // -435428106
    }
}

//定义:Person类
class Person {

    protected String name;
    Person(String name){
        this.name = name;
    }

    // 显示更有意义的字符串:
    @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();
    }
}

2.2.4 final

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

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

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

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

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字段就不可修改。

2.3 abstract

2.3.1 抽象方法、抽象类

如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法。必须把Person类本身也声明为**abstract类**,才能正确编译它:

abstract class Person {
    public abstract void run();
}

我们无法实例化一个抽象类:

Person p = new Person(); // 编译错误

无法实例化的抽象类有什么用?

因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。

例如,Person类定义了抽象方法run(),那么,在实现子类Student的时候,就必须覆写run()方法。

2.3.2 面向抽象编程

当我们定义了抽象类Person,以及具体的StudentTeacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例:

Person s = new Student();
Person t = new Teacher();

这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person类型变量的具体子类型:

// 不关心Person变量的具体子类型:
s.run();
t.run();

这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程

面向抽象编程的本质就是:

  • 上层代码只定义规范(例如:abstract class Person);
  • 不需要子类就可以实现业务逻辑(正常编译);
  • 具体的业务逻辑由不同的子类实现,调用者并不关心。

2.4 接口

2.4.1 interface

在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。

如果一个抽象类没有字段,所有方法全部都是抽象方法:

abstract class Person {
    public abstract void run();
    public abstract String getName();
}

就可以把该抽象类改写为接口interface(比抽象类还要抽象)

在Java中,使用interface可以声明一个接口:

interface Person {
    void run();
    String getName();
}

当一个具体的class去实现一个interface时,需要使用implements关键字。举个例子:

              // 【√】
class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    @Override // 覆写
    public void run() {
        System.out.println(this.name + " run");
    }

    @Override // 覆写
    public String getName() {
        return this.name;
    }
}

我们知道,在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface,例如:

class Student implements Person, Hello { // 实现了两个interface
    ...
}

2.4.2 区分

注意区分术语:

  • Java的接口特指interface的定义,表示一个接口类型和一组方法签名
  • 编程接口泛指接口规范,如方法签名,数据格式,网络协议等

抽象类和接口的对比

abstract classinterface
继承只能extends一个class可以implements多个interface
字段可以定义实例字段不能定义实例字段
抽象方法可以定义抽象方法可以定义抽象方法
非抽象方法可以定义非抽象方法可以定义default方法

2.4.3 接口继承

一个interface可以继承自另一个interfaceinterface继承自interface使用extends,它相当于扩展了接口的方法。

interface Hello {
    void hello();
}

interface Person extends Hello {
    void run();
    String getName();
}

Person接口现在实际上有3个抽象方法签名,其中一个来自继承的Hello接口。

2.4.4 default方法

当我们需要给接口新增一个方法时,会涉及到修改全部子类(因为需要覆写)

如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。例如:

package Main;

public class Main {
    public static void main(String[] args) {
        Person p = new Student("Xiao Ming"); // 用更高层次的Person引用Student实例
        p.run();
    }
}

// 接口
interface Person {
    String getName();
    default void run() { // default方法,子类可以不用覆写【√】
        System.out.println(getName() + " run");
    }
}

// 类
class Student implements Person {
    private String name; // 字段

    public Student(String name) { // 构造函数
        this.name = name;
    }

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

2.4 包

2.4.0 简介

Java内建的package机制是为了避免class命名冲突;

JDK的核心类使用java.lang包,编译器会自动导入;

JDK的其它常用类定义在java.util.*java.math.*java.text.*,……;

包名推荐使用倒置的域名,例如org.apache

2.4.1 package

在Java中,我们使用package解决名字冲突

Java定义了一种名字空间,称之为包:package。一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名

例如:

  • 不同人写的Person类

    • 小明的Person类存放在包ming下面,因此,完整类名是ming.Person

      • 小明的Person.java文件:

        package ming; // 申明包名ming
        
        public class Person {
        }
        
    • 小红的Person类存放在包hong下面,因此,完整类名是hong.Person

  • 不同人写的Arrays类

    • 小军的Arrays类存放在包mr.jun下面,因此,完整类名是mr.jun.Arrays
    • JDK的Arrays类存放在包java.util下面,因此,完整类名是java.util.Arrays

在Java虚拟机执行的时候,JVM只看完整类名,因此,只要包名不同,类就不同

2.4.2 文件结构

包可以是多层结构,用.隔开。例如:java.util

特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。

我们还需要按照包结构把上面的Java文件组织起来。假设以package_sample作为根目录,src作为源码目录,那么所有文件结构就是:

image-20201003212436531

即所有Java文件对应的目录层次要和包的层次一致。

2.4.3 包作用域

位于同一个包的类,可以访问包作用域的字段和方法。不用publicprotectedprivate修饰的字段和方法就是包作用域。例如,Person类定义在hello包下面:

package hello;

public class Person {
    // 包作用域: 
    void hello() {
        System.out.println("Hello!");
    }
}

Main类也定义在hello包下面:

package hello;

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.hello(); // 可以调用,因为Main和Person在同一个包
    }
}

文件结构:

image-20201003213502842

2.4.4 Import

在一个class中,我们总会引用其他的class。例如,小明的ming.Person类,如果要引用小军的mr.jun.Arrays类,他有三种写法:

  • 第一种,直接写出完整类名(显然很麻烦)

  • 第二种写法是用import语句,导入小军的Arrays,然后写简单类名:

    // Person.java
    package ming;
    
    // 导入完整类名:
    import mr.jun.Arrays;
    
    public class Person {
        public void run() {
            Arrays arrays = new Arrays();
        }
    }
    
    • 在写import的时候,可以使用*,表示把这个包下面的所有class都导入进来(但不包括子包的class

      // 导入mr.jun包的所有class:
      import mr.jun.*;
      
      • 我们一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays类属于哪个包。

编写class的时候,编译器会自动帮我们做两个import动作:

  • 默认自动import当前package的其他class
  • 默认自动import java.lang.*

2.4.5 倒置的域名

为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:

  • org.apache
  • org.apache.commons.log
  • com.liaoxuefeng.sample

子包就可以根据功能自行命名。

要注意不要和java.lang包的类重名,即自己的类不要使用这些名字:

  • String
  • System
  • Runtime
  • ...

要注意也不要和JDK常用类重名:

  • java.util.List
  • java.text.Format
  • java.math.BigInteger
  • ...

好的,先学到这里吧,去多多实战一下~ ——2020年10月3日21:44:27