面对对象编程

Java是一门面对对象编程(Object-Oriented Programming)语言,OOP是一种设计思想,意味着我们把对象作为程序的基本单位,而每个对象包含了自己的属性(attributes)和方法(methods)。面对对象编程有以下特点:

  1. 封装(Encapsulation):对外部世界隐藏内部实现的细节。
  2. 继承(Inheritance):继承使子类具有父类的属性和方法,而不用编写相同的代码。
  3. 多态(Polymorphism):为不同的数据类型的实现提供统一的接口。

使用OOP有以下的优点:

  1. 提高软件开发的生产效率
  2. 增强软件的可维护性
  3. 提高软件的质量

在之前的内容中,我们已经接触了内置的类,比如String,Arrays,List。在这一章,我们就来学习如何设计自己的类并创建实例。

类和实例

每个类都有自己的属性(attribute)和方法(method),比如一个人的身高、体重和年龄,这些都是属性,而吃饭、说活和睡觉都是方法。

在下面的例子中,我们创建了一个员工类,其中包括名字和工资属性,然后还有拿工资和离职的方法:

public class Employee {
    String name = "Kevin";
    int salary = 10000;
    public int getSalary() {
        return salary;
    }
    public void quitJob() { 
        System.out.println("I'm out!");
    }
}

类是一个实例(Object)的设计蓝图,在创建实例的时候我们只要调用类名,然后加上括号就可以了。如果想要调用属性或方法,直接在实例的后面加上句号(.)紧跟着属性或方法的名字即可:

public class Employee { 
...
public static void main(String[] args) {
    Employee employee = new Employee();
    employee.quitJob();
    System.out.println(employee.name);
}
...
}    

在这个例子中,我们创建了Employee类,定义了两个属性和方法。并创建一个employee变量指向Employee的实例,然后使用employee.quitJob()和employee.name调用其方法和属性。之前的教学都是从方法的角度来创建程序,而学会类之后,我们可以从更高的层面来设计程序,让我们的软件更系统化。

constructor和this

每个类中可以有一个特殊的构建函数,它用于初始化实例。构建函数是一个实例被创建时最先被调用的函数,每次创建实例的时候,它的构建函数都会被调用。

public class Student {
    String name;
    public Student() {
        name = "Samuel";
    }
    public static void main(String[] args) { 
        Student student  = new Student();
        System.out.println(student.name); 
    }
}

在上面这个例子中,pulibc Student就是构建函数,构建函数不能有返回值,而且函数名必须和类名一致。在main函数中,我们创建Student实例之后,调用name属性就会返回Samuel,因为在构建函数中,我们的name被初始化成Samuel了。构建函数中也可以带入参数:

public class Student { 
    String name;
    public Student(String name) { 
        this.name = name;
    }
    public static void main(String[] args) {
        Student student = new Student("Daniel");
        System.out.println(student.name); // Daniel 
    }
}

在这个构建函数中,我们传入参数字符串name,然后使用这个字符串初始化类的属性name。这里有个关键词this,它永远指向创建的实例本身,所以this.name就是属性name,而this.name = name后面的name指的则是参数name。我们再来看一个例子:

public class Car { 
    String model;
    int year;
    public Car(String m) { 
        this.model = m;
    }
    public Car(String m, int y) { 
        this.model = m;
        this.year = y;
    }
    public static void main(String[] args) { 
        Car car = new Car("BMW X5");
        System.out.printf("Model: %s, Year: %s\n", car.model, car.year); 
        car = new Car("Subaru WRX", 2019);
        System.out.printf("Model: %s, Year: %s\n", car.model, car.year); 
    }
}  

在这个Car类中,我们创建了两个构建函数,一个只需要一个参数m,而另一个则需要两个参数m和y来初始化车子的model和year,那么我们在创建实例的时候,我们就可以根据自己的需求来调用不同的构建函数。在main函数中,第一次调用构建函数我们使用的就是第一个构建函数,在这个函数中,model被初始化了,而year没有任何改变。而在第二次的调用函数中,我们传入了两个参数,这样model和year就都能被赋值了。这种函数名相同,但是参数不同的情况,在上一章方法中也提到过,叫做重载(Overload)。

属性和方法的修饰符(static, public, private)

main函数中以public static开头,其中public static就是方法的修饰符。

我们先来了解一下static修饰符,static修饰符可以让方法的使用范围变为全局,如果我们要使用此方法,则不需要创建实例,可以直接使用:

public class Calculator {
    public static int multiply(int num1, int num2) {
        return num1 * num2;
    }
    public static void main(String[] args) {
        int result = Calculator.multiply(3, 9); 
        System.out.println(result); // 27
    }
}

修饰访问权限的关键词则是 public、private、protected。被定义为public的class, attributes, method可以被任何类访问,如果是private那么就无法被其他类访问,protected适用于继承关系间的类,被定义为protected的属性和方法可以被子类访问,父类子类相关的内容我们会在下一章详细探讨。

在下面的类中,我们定义了一个public属性和public类,如果要使用它们,则可以在其他类中直接通过实例调用:

# Student.java
public class Student {
     public name = "David";
     public void setName(String name) { 
         this.name = name;
     }
}
# Main.java
public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        System.out.println(student.name); // David
        student.setName("Enoch");
        System.out.println(student.name); // Enoch
    }
}

如果把 name 的访问权限改为 private,那么main函数中的代码就无法运行了,因为private属性只能在类的内部被访问到,但我们可以创建一个新的public方法来帮助用户拿到student的值:

# Student.java
public class Student {
     private name = "David";
     public void setName(String name) { 
         this.name = name;
     }
     public String getName() { 
         return this.name;
     }
}
# Main.java
public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        System.out.println(student.getName()); // David
        student.setName("Enoch");
        System.out.println(student.getName()); // Enoch
    }
}

如果没有修饰符,比如 String name() 这样的方法,既没有public也没有private,那么同一个包中的类都可以访问到,子类也可以访问到,但是其他包的类就无法访问此方法了。具体的访问范围可以参考以下表格:

------------+-------+---------+--------------+--------------+--------
            | Class | Package | Subclass     | Subclass     |Outside|
            |       |         |(same package)|(diff package)|Class  |
————————————+———————+—————————+——————————----+—————————----—+————————
public      | Yes   |  Yes    |    Yes       |    Yes       |   Yes |    
————————————+———————+—————————+—————————----—+—————————----—+————————
protected   | Yes   |  Yes    |    Yes       |    Yes       |   No  |    
————————————+———————+—————————+————————----——+————————----——+————————
default     | Yes   |  Yes    |    Yes       |    No        |   No  |
————————————+———————+—————————+————————----——+————————----——+————————
private     | Yes   |  No     |    No        |    No        |   No  |
------------+-------+---------+--------------+--------------+--------

包的调用(Packages)

现在我们已经学会了创建自己的类和实例,并学会了调用其中的attribute和method,那么我们接下来学习调用Java内置的类,并调用常用的方法来加深理解类的使用。

包的概念

在调用其他类之前,我们先来了解一个必须掌握的概念,就是包 (package)。如果我想写一个类叫Student,但是另一个同事也要写一个Student类,这就会产生名字冲突,为了解决这个问题,我们则需要 package 来解决此问题。

Java定义了一种命名空间,叫做包(package)。一个类总是归属于某一个包,所以一个类的完整名字便是 package_name.class_name。比如我的Student类放在harvard下面,同事的Student类放在stanford下面,那么这两个完整的类名分别就是 harvard.Student 和 stanford.Student。JDK中的 Arrays 类存放在 java.util 包中,那么完整类名就是 java.util.Arrays。

要注意的是,我们需要根据包名来组织文件目录,假设我们的 java_project 是根目录,src是源代码目录,那么就要在java_project中创建以下目录结构:

java_project
└─ src
    ├─ harvard
    │  └─ Student.java
    └─ stanford
       └─ Student.java

位于同一个包的类,可以访问包作用域的属性和方法,没有public、private、protected修饰的属性和方法就是包作用域。比如我们在harvard中定义一个包作用域的 greet() 方法:

package harvard;
public class Student {
    void greet() {
        System.out.println("Hello, I'm a student from Harvard.");
    }
}

然后我们在harvard包中再创建一个Main类来调用Student的方法:

package harvard;
public class Main {
    public static void main(String[] args) {
        Student hStudent = new Student();
        hStudent.greet();
   }
}

可以看到只要是在同一个包中,默认的方法是可以被调用的。如果我们想要在其他的包中调用greet方法,我们需要使用import关键字,紧跟着完整类名。比如我们在stanford中创建一个Main来调用harvard包中的Student:

package stanford;
import harvard.Student;
public class Main {
    public static void main(String[] args) {
        Student hStudent = new Student();
        hStudent.greet();
    }
}

但是这个程序是会发生错误的,因为只有public的方法才能被其他包中的类调用,所以我们要把 harvard.Student 中的greet作用域变为public,这样程序才能运行:

public void greet() {
    System.out.println("Hello, I'm a student from Harvard.");
}

JDK类的使用

import可以调用自己写的包,也可以调用 JDK中内置的类,在下面的代码中我们就调用一个常用的数据结构哈希表(HashMap)来演示调用内置类的方式:

import java.util.HashMap;
public class Main {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("BMW", 10000);
        System.out.println(map.get("BMW")); // 10000
    }
}

HashMap是一个常用的数据结构,可以将关键字和它所对应的值连接起来,代码中,我们将BMV字符串和整数10000对应起来,下次想要拿到BMW对应的数字时,直接使用map.get(“BMW”)即可。数据结构是另外一个大的话题,已经超出了基础的教学,这里不需要深入理解。

除了HashMap,java.util包中还有很多类,比如ArrayList, Date, Iterator等等,如果想要将其中的类全部导入,使用星号(*)即可,所以完整的导入便是 import java.util.*。

在这一章我们学会了如何创建自己的类,并调用自己的类和内置的类,在下一章我们会深入讲解面对对象编程的重要概念,比如继承和接口。

实践练习

创建一个Car类,其中包含brand,model,year属性。这个类初始化的时候必须要传入品牌名字(比如Subaru),初始化的时候车的型号和生产年要被默认设定为”xxx“和0,Car类还需要提供内置方法用来修改车型号和生产年,还有可以打印出完整车信息的方法。

请根据要求定义出这个完整的类,并在另一个类中调用此类,创建一个实例,然后使用内置方法修改车的型号和生产年,最后使用类的方法打印出车的完整信息。