# Java 面向对象

深入理解 Java 基本数据类型 (opens new window)中我们了解 Java 中支持的基本数据类型(值类型)。本文开始讲解 Java 中重要的引用类型——类。

📦 本文以及示例源码已归档在 javacore (opens new window)

# 1. 面向对象

每种编程语言,都有自己的操纵内存中元素的方式。

Java 中提供了基本数据类型 (opens new window),但这还不能满足编写程序时,需要抽象更加复杂数据类型的需要。因此,Java 中,允许开发者通过类(类的机制下面会讲到)创建自定义类型。

有了自定义类型,那么数据类型自然会千变万化,所以,必须要有一定的机制,使得它们仍然保持一些必要的、通用的特性。

Java 世界有一句名言:一切皆为对象。这句话,你可能第一天学 Java 时,就听过了。这不仅仅是一句口号,也体现在 Java 的设计上。

  • 首先,所有 Java 类都继承自 Object 类(从这个名字,就可见一斑)。
  • 几乎所有 Java 对象初始化时,都要使用 new 创建对象(基本数据类型 (opens new window)、String、枚举特殊处理),对象存储在堆中。
// 下面两
String s = "abc";
String s = new String("abc");

其中,String s 定义了一个名为 s 的引用,它指向一个 String 类型的对象,而实际的对象是 “abc” 字符串。这就像是,使用遥控器(引用)来操纵电视机(对象)。

与 C/C++ 这类语言不同,程序员只需要通过 new 创建一个对象,但不必负责销毁或结束一个对象。负责运行 Java 程序的 Java 虚拟机有一个垃圾回收器,它会监视 new 创建的对象,一旦发现对象不再被引用,则会释放对象的内存空间。

# 1.1. 封装

封装(Encapsulation)是指一种将抽象性函式接口的实现细节部份包装、隐藏起来的方法。

封装最主要的作用在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。

适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。

封装的优点:

  • 良好的封装能够减少耦合。
  • 类内部的结构可以自由修改。
  • 可以对成员变量进行更精确的控制。
  • 隐藏信息,实现细节。

实现封装的步骤:

  1. 修改属性的可见性来限制对属性的访问(一般限制为 private)。
  2. 对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问。

# 1.2. 继承

继承是 java 面向对象编程技术的一块基石,因为它允许创建分等级层次的类。

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

现实中的例子:

狗和鸟都是动物。如果将狗、鸟作为类,它们可以继承动物类。

img

类的继承形式:

class 父类 {}

class 子类 extends 父类 {}

# 继承类型

img

# 继承的特性

  • 子类拥有父类非 private 的属性、方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。
  • Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 A 类继承 B 类,B 类继承 C 类,所以按照关系就是 C 类是 B 类的父类,B 类是 A 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。

# 继承关键字

继承可以使用 extends 和 implements 这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承 object(这个类在 java.lang 包中,所以不需要 import)祖先类。

# 1.3. 多态

刚开始学习面向对象编程时,容易被各种术语弄得云里雾里。所以,很多人会死记硬背书中对于术语的定义。

但是,随着应用和理解的深入,应该会渐渐有更进一步的认识,将其融汇贯通的理解。

学习类之前,先让我们思考一个问题:Java 中为什么要引入类机制,设计的初衷是什么?

Java 中提供的基本数据类型,只能表示单一的数值,这用于数值计算,还 OK。但是,如果要抽象模拟现实中更复杂的事物,则无法做到。

试想,如果要让你抽象狗的数据模型,怎么做?狗有眼耳口鼻等器官,有腿,狗有大小,毛色,这些都是它的状态,狗会跑、会叫、会吃东西,这些是它的行为。

类的引入,就是为了抽象这种相对复杂的事物。

对象是用于计算机语言对问题域中事物的描述。对象通过方法和属性来分别描述事物所具有的行为和状态。

类是用于描述同一类的对象的一个抽象的概念,类中定义了这一类对象所具有的行为和状态。

类可以看成是创建 Java 对象的模板。

什么是方法?扩展阅读:面向对象编程的弊端是什么? - invalid s 的回答 (opens new window)

# 2. 类

与大多数面向对象编程语言一样,Java 使用 class (类)关键字来表示自定义类型。自定义类型是为了更容易抽象现实事物。

在一个类中,可以设置一静一动两种元素:属性(静)和方法(动)。

  • 属性(有的人喜欢称为成员、字段) - 属性抽象的是事物的状态。类属性可以是任何类型的对象。
  • 方法(有的人喜欢称为函数) - 方法抽象的是事物的行为。

类的形式如下:

img

# 3. 方法

# 3.1. 方法定义

修饰符 返回值类型 方法名(参数类型 参数名){
    ...
    方法体
    ...
    return 返回值;
}

方法包含一个方法头和一个方法体。下面是一个方法的所有部分:

  • **修饰符:**修饰符,这是可选的,告诉编译器如何调用该方法。定义了该方法的访问类型。
  • **返回值类型 :**方法可能有返回值。如果没有返回值,这种情况下,返回值类型应设为 void。
  • **方法名:**是方法的实际名称。方法名和参数表共同构成方法签名。
  • **参数类型:**参数像是一个占位符。当方法被调用时,传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数。
  • **方法体:**方法体包含具体的语句,定义该方法的功能。

示例:

public static int add(int x, int y) {
   return x + y;
}

# 3.2. 方法调用

Java 支持两种调用方法的方式,根据方法是否返回值来选择。

当程序调用一个方法时,程序的控制权交给了被调用的方法。当被调用方法的返回语句执行或者到达方法体闭括号时候交还控制权给程序。

当方法返回一个值的时候,方法调用通常被当做一个值。例如:

int larger = max(30, 40);

如果方法返回值是 void,方法调用一定是一条语句。例如,方法 println 返回 void。下面的调用是个语句:

System.out.println("Hello World");

# 3.3. 构造方法

每个类都有构造方法。如果没有显式地为类定义任何构造方法,Java 编译器将会为该类提供一个默认构造方法。

在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。

public class Puppy{
    public Puppy(){
    }

    public Puppy(String name){
        // 这个构造器仅有一个参数:name
    }
}

# 4. 变量

Java 支持的变量类型有:

  • 局部变量 - 类方法中的变量。
  • 实例变量(也叫成员变量) - 类方法外的变量,不过没有 static 修饰。
  • 类变量(也叫静态变量) - 类方法外的变量,用 static 修饰。

特性对比:

局部变量 实例变量(也叫成员变量) 类变量(也叫静态变量)
局部变量声明在方法、构造方法或者语句块中。 实例变量声明在方法、构造方法和语句块之外。 类变量声明在方法、构造方法和语句块之外。并且以 static 修饰。
局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁。 实例变量在对象创建的时候创建,在对象被销毁的时候销毁。 类变量在第一次被访问时创建,在程序结束时销毁。
局部变量没有默认值,所以必须经过初始化,才可以使用。 实例变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定。 类变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块中初始化。
对于局部变量,如果是基本类型,会把值直接存储在栈;如果是引用类型,会把其对象存储在堆,而把这个对象的引用(指针)存储在栈。 实例变量存储在堆。 类变量存储在静态存储区。
访问修饰符不能用于局部变量。 访问修饰符可以用于实例变量。 访问修饰符可以用于类变量。
局部变量只在声明它的方法、构造方法或者语句块中可见。 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符可以使实例变量对子类可见。 与实例变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为 public 类型。
实例变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObejectReference.VariableName。 静态变量可以通过:ClassName.VariableName 的方式访问。
无论一个类创建了多少个对象,类只拥有类变量的一份拷贝。
类变量除了被声明为常量外很少使用。

# 4.1. 变量修饰符

  • 访问级别修饰符 - 如果变量是实例变量或类变量,可以添加访问级别修饰符(public/protected/private)
  • 静态修饰符 - 如果变量是类变量,需要添加 static 修饰
  • final - 如果变量使用 fianl 修饰符,就表示这是一个常量,不能被修改。

# 4.2. 创建对象

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

  • 声明:声明一个对象,包括对象名称和对象类型。
  • 实例化:使用关键字 new 来创建一个对象。
  • 初始化:使用 new 创建对象时,会调用构造方法初始化对象。
public class Puppy{
   public Puppy(String name){
      //这个构造器仅有一个参数:name
      System.out.println("小狗的名字是 : " + name );
   }
   public static void main(String[] args){
      // 下面的语句将创建一个Puppy对象
      Puppy myPuppy = new Puppy( "tommy" );
   }
}

# 4.3. 访问实例变量和方法

/* 实例化对象 */
ObjectReference = new Constructor();
/* 访问类中的变量 */
ObjectReference.variableName;
/* 访问类中的方法 */
ObjectReference.methodName();

# 5. 访问权限控制

# 5.1. 代码组织

当编译一个 .java 文件时,在 .java 文件中的每个类都会输出一个与类同名的 .class 文件。

MultiClassDemo.java 示例:

class MultiClass1 {}

class MultiClass2 {}

class MultiClass3 {}

public class MultiClassDemo {}

执行 javac MultiClassDemo.java 命令,本地会生成 MultiClass1.class、MultiClass2.class、MultiClass3.class、MultiClassDemo.class 四个文件。

Java 可运行程序是由一组 .class 文件打包并压缩成的一个 .jar 文件。Java 解释器负责这些文件的查找、装载和解释。Java 类库实际上是一组类文件(.java 文件)。

  • 其中每个文件允许有一个 public 类,以及任意数量的非 public 类
  • public 类名必须和 .java 文件名完全相同,包括大小写。

程序一般不止一个人编写,会调用系统提供的代码、第三方库中的代码、项目中其他人写的代码等,不同的人因为不同的目的可能定义同样的类名/接口名,这就是命名冲突。

Java 中为了解决命名冲突问题,提供了包(package)和导入(import)机制。

# package

包(package)的原则:

  • 包类似于文件夹,文件放在文件夹中,类和接口则放在包中。为了便于组织,文件夹一般是一个有层次的树形结构,包也类似。
  • 包名以逗号 . 分隔,表示层次结构。
  • Java 中命名包名的一个惯例是使用域名作为前缀,因为域名是唯一的,一般按照域名的反序来定义包名,比如,域名是:apache.org,包名就以 org.apache 开头。
  • **包名和文件目录结构必须完全匹配。**Java 解释器运行过程如下:
    • 找出环境变量 CLASSPATH,作为 .class 文件的根目录。
    • 从根目录开始,获取包名称,并将逗号 . 替换为文件分隔符(反斜杠 /),通过这个路径名称去查找 Java 类。

# import

同一个包下的类之间互相引用是不需要包名的,可以直接使用。但如果类不在同一个包内,则必须要知道其所在的包,使用有两种方式:

  • 通过类的完全限定名
  • 通过 import 将用到的类引入到当前类

通过类的完全限定名示例:

public class PackageDemo {
    public static void main (String[]args){
        System.out.println(new java.util.Date());
        System.out.println(new java.util.Date());
    }
}

通过 import 导入其它包的类到当前类:

import java.util.Date;

public class PackageDemo2 {
    public static void main(String[] args) {
        System.out.println(new Date());
        System.out.println(new Date());
    }
}

说明:以上两个示例比较起来,显然是 import 方式,代码更加整洁。

扩展阅读:https://www.cnblogs.com/swiftma/p/5628762.html

# 5.2. 访问权限修饰关键字

访问权限控制的等级,从最大权限到最小权限依次为:

public > protected > 包访问权限(没有任何关键字)> private
  • public - 表示任何类都可以访问;
  • 包访问权限 - 包访问权限,没有任何关键字。它表示当前包中的所有其他类都可以访问,但是其它包的类无法访问。
  • protected - 表示子类可以访问,此外,同一个包内的其他类也可以访问,即使这些类不是子类。
  • private - 表示其它任何类都无法访问。

# 6. 接口

接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。

接口,不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。

Java 标准类库中,定义了非常多的接口,比如 java.util.List

public interface Comparable<T> {
    public int compareTo(T o);
}

# 7. 抽象类

抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。

Java 标准库中,比如 collection 框架,很多通用部分就被抽取成为抽象类,例如 java.util.AbstractList

  1. 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
  2. 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
  3. 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
  4. 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
  5. 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

# 8. 参考资料