Java 始终位列全球最受欢迎的三大编程语言之一。它在企业软件开发、Android 移动应用以及大规模 Web 应用等领域的采用无出其右。其强大的类型系统、庞大的生态以及“一次编写,到处运行 ”的能力,使其在构建健壮、可扩展系统方面尤为吸引人。本文将探讨 Java 的面向对象编程特性如何帮助开发者有效发挥这些优势,通过合理的代码组织与复用,构建可维护、可扩展的应用程序。
关于组织与运行 Java 代码的说明
在开始写代码之前,先进行一些准备工作。
与语法一样,Java 对代码组织也有严格规定。
首先,每个 public 类必须位于其各自的文件中,文件名与类名完全一致,并以 .java 为扩展名。因此,如果我要写一个 Laptop 类,文件名必须是 Laptop.java—区分大小写。您可以在同一文件中包含非 public 的类,但最好将它们分开。我知道我们有点超前——在编写类之前就讨论类的组织——但事先大致了解各部分应放在哪里是个好主意。
所有 Java 项目都必须有一个包含 Main 类的 Main.java 文件。这里是通过创建对象来测试各个类的地方。
要运行 Java 代码,我们将使用 IntelliJ IDEA ,这是一款流行的 Java IDE。安装 IntelliJ 后:
创建一个新的 Java 项目(File > New > Project)
右键单击 src 文件夹以创建 Main.java 文件,并粘贴以下内容:
public class Main {
public static void main ( String [ ] args) {
}
} 技术支持
每当我们讨论类时,代码写在除 Main.java 之外的文件里。但如果我们讨论创建和测试 对象 ,就切换到 Main.java 。
要运行程序,您可以单击 main 方法旁边的绿色播放按钮:
输出将显示在底部的 Run 工具窗口中。
如果您完全是 Java 新手,请先查看我们的 Introduction to Java 课程 ,在继续之前了解 Java 的数据类型和控制流基础。
否则,我们直接开始。
Java 类与对象
那么,类究竟是什么?
类是 Java 中用于表示现实世界概念的编程构造。举例来说,考虑这个 MenuItem 类(在 IDE 中创建文件来编写该类):
public class MenuItem {
public String name;
public double price;
} 技术支持
该类为我们提供了一个蓝图或模板,用于表示餐厅中的各种菜单项。通过更改类的两个属性 name 和 price ,我们可以创建无数菜单 对象 ,例如汉堡或沙拉。
因此,要在 Java 中创建一个类,您需要先写一行用于描述类访问级别的代码( private 、 public 或 protected ),紧随其后的是类名。随后在大括号内概述类的各个属性。
但我们如何创建属于该类的对象呢?Java 通过构造方法来实现这一点:
public class MenuItem {
public String name;
public double price;
public MenuItem ( String name, double price) {
this . name = name;
this . price = price;
}
} 技术支持
构造函数是在我们从类创建新对象时被调用的特殊方法。它会使用我们提供的值来初始化对象的属性。在上面的示例中,构造函数接收名称与价格参数,并使用 “this” 关键字将它们赋给未来对象实例的字段。
构造函数的语法不同于其他类方法,因为它不需要指定返回类型。此外,构造函数的名称必须与类名相同,并且其参数应与您在类定义后声明的属性数量相对应。上面的构造函数创建了两个属性,因为我们在类定义后声明了两个:name 和 price。
完成类及其构造函数的编写后,您可以在 main 方法中创建该类的实例(对象):
public class Main {
public static void main ( String [ ] args) {
MenuItem burger = new MenuItem ( "Burger" , 3.5 ) ;
MenuItem salad = new MenuItem ( "Salad" , 2.5 ) ;
System . out. println ( burger. name + ", " + burger. price) ;
}
} 技术支持
输出:
Burger , 3.5 技术支持
上面我们创建了两个 MenuItem 对象并赋给变量 burger 和 salad 。按照 Java 的要求,变量类型必须声明,这里就是 MenuItem 。随后,为了创建类的实例,我们写上 new 关键字并调用构造方法。
除了构造函数,您还可以创建常规方法来赋予类以行为。例如,下面我们添加一个方法来计算含税总价:
public class MenuItem {
public String name;
public double price;
public MenuItem ( String name, double price) {
this . name = name;
this . price = price;
}
public double getPriceAfterTax ( ) {
double taxRate = 0.08 ;
return price + ( price * taxRate) ;
}
} 技术支持
现在我们可以计算含税总价:
public class Main {
public static void main ( String [ ] args) {
MenuItem burger = new MenuItem ( "Burger" , 3.5 ) ;
System . out. println ( "Price after tax: $" + burger. getPriceAfterTax ( ) ) ;
}
}
技术支持
输出:
Price after tax: $3.78 技术支持
封装
类的目的在于为创建对象提供蓝图。这些对象随后会被其他脚本或程序使用。例如,我们的 MenuItem 对象可以被一个用户界面使用,在屏幕上展示它们的名称、价格和图片。
因此,我们必须以一种只能按预期方式使用其实例的方式来设计类。现在我们的 MenuItem 类非常基础且容易出错。有人可能会创建一些荒谬的属性,比如负价格的苹果派或价值百万美元的三明治:
MenuItem applePie = new MenuItem ( "Apple Pie" , - 5.99 ) ;
MenuItem sandwich = new MenuItem ( "Sandwich" , 1000000 ) ;
System . out. println ( "Apple pie price: $" + applePie. price) ;
System . out. println ( "Sandwich price: $" + sandwich. price) ;
技术支持
所以,编写完类之后的首要任务是通过限制属性的创建与访问方式来保护它们。首先,我们只允许 price 为正值,并设置一个上限,避免意外显示离谱的高价。
Java 允许我们通过 setter 方法来实现:
public class MenuItem {
private String name;
private double price;
private static final double MAX_PRICE = 100.0 ;
public MenuItem ( String name, double price) {
this . name = name;
setPrice ( price) ;
}
public void setPrice ( double price) {
if ( price < 0 ) {
throw new IllegalArgumentException ( "Price cannot be negative" ) ;
}
if ( price > MAX_PRICE ) {
throw new IllegalArgumentException ( "Price cannot exceed $" + MAX_PRICE ) ;
}
this . price = price;
}
} 技术支持
让我们来看看上面代码块中的新内容:
1. 我们通过添加 private 关键字将属性设为私有。这意味着它们只能在 MenuItem 类内部访问。封装始于这一步关键操作。
2. 我们新增了一个常量 MAX_PRICE ,它:
是 private(仅在类内可访问)
是 static(在所有实例间共享)
是 final(初始化后不可更改)
被设置为 100.0 美元,作为一个合理的最高价格
3. 我们添加了 setPrice() 方法,它:
接收一个 price 参数
校验价格不为负数
校验价格不超过 MAX_PRICE
若校验失败,抛出带有描述信息的 IllegalArgumentException
仅在通过所有校验后才设置价格
4. 我们修改了构造函数,使用 setPrice() 而不是直接赋值价格。这确保在创建对象时进行价格校验。
我们刚刚实现了优良面向对象设计的核心支柱之一—— 封装 。该范式强制进行数据隐藏和对对象属性的受控访问,确保内部实现细节免受外部干扰,只能通过定义良好的接口进行修改。
让我们通过将封装应用到 name 属性上来进一步强调这一点。想象我们有一家咖啡店,只提供拿铁、卡布奇诺、浓缩、美式和摩卡。
因此,我们的菜单项名称只能是该列表中的某一个。以下是如何在代码中强制执行:
. . .
private static final String [ ] VALID_NAMES = { "latte" , "cappuccino" , "espresso" , "americano" , "mocha" } ;
private String name;
public void setName ( String name) {
String lowercaseName = name. toLowerCase ( ) ;
for ( String validName : VALID_NAMES ) {
if ( validName. equals ( lowercaseName) ) {
this . name = name;
return ;
}
}
throw new IllegalArgumentException ( "Invalid drink name. Must be one of: " + String . join ( ", " , VALID_NAMES ) ) ;
} 技术支持
上述代码实现了咖啡店菜单项名称的校验。我们来分解一下:
1. 首先,它定义了一个私有的静态常量数组 VALID_NAMES,其中包含唯一允许的饮品名称:latte、cappuccino、espresso、americano 和 mocha。该数组:
private:仅在类内可访问
static:在所有实例间共享
final:初始化后不可修改
2. 它声明了一个私有的 String 类型 name 字段用于存储饮品名称
3. setName() 方法实现校验逻辑:
接收一个 String 类型的 name 参数
转换为小写以实现不区分大小写的比较
遍历 VALID_NAMES 数组
如果找到匹配,则设置名称并返回
如果未找到匹配,则抛出 IllegalArgumentException,并给出包含所有合法选项的描述信息
以下是目前完整的类:
public class MenuItem {
private String name;
private double price;
private static final String [ ] VALID_NAMES = { "latte" , "cappuccino" , "espresso" , "americano" , "mocha" } ;
private static final double MAX_PRICE = 100.0 ;
public MenuItem ( String name, double price) {
setName ( name) ;
setPrice ( price) ;
}
public void setName ( String name) {
String lowercaseName = name. toLowerCase ( ) ;
for ( String validName : VALID_NAMES ) {
if ( validName. equals ( lowercaseName) ) {
this . name = name;
return ;
}
}
throw new IllegalArgumentException ( "Invalid drink name. Must be one of: " + String . join ( ", " , VALID_NAMES ) ) ;
}
public void setPrice ( double price) {
if ( price < 0 ) {
throw new IllegalArgumentException ( "Price cannot be negative" ) ;
}
if ( price > MAX_PRICE ) {
throw new IllegalArgumentException ( "Price cannot exceed $" + MAX_PRICE ) ;
}
this . price = price;
}
}
技术支持
在保护了属性的创建方式之后,我们也要保护它们的访问方式。可以通过 getter 方法来实现:
public class MenuItem {
. . .
public String getName ( ) {
return name;
}
public double getPrice ( ) {
return price;
}
} 技术支持
Getter 方法为类的私有属性提供受控访问。它们解决了直接访问属性可能导致非预期修改、破坏封装的问题。
例如,没有 getter 时,我们可能会直接访问属性:
MenuItem item = new MenuItem ( "Latte" , 4.99 ) ;
String name = item. name;
item. name = "INVALID" ; 技术支持
使用 getter 时,我们可强制正确访问:
MenuItem item = new MenuItem ( "Latte" , 4.99 ) ;
String name = item. getName ( ) ;
技术支持
这种封装:
通过防止无效修改来保护数据完整性
允许我们在不影响使用该类代码的情况下更改内部实现
提供一个可在需要时加入额外逻辑的统一访问入口
让代码更易维护且更不易出错
继承
我们的类开始变得不错,但它仍存在不少问题。比如对于一家提供多种菜品和饮品的大型餐厅来说,这个类不够灵活。
如果我们想添加不同类型的食物项,会遇到诸多挑战。有些菜品可以打包带走,有些需要立即食用。菜单项可能有不同价格和折扣。菜品可能需要温度监控或特殊存储。饮品可冷可热并支持自定义配料。条目可能需要过敏原信息与分量选项。当前系统无法处理这些多样化需求。
继承为这些问题提供了优雅的解决方案。它允许我们通过定义一个包含通用属性的基础 MenuItem 类,然后创建继承这些基础并添加独特特性的子类,来构建菜单项的专门版本。
例如,我们可以有一个包含温度选项的 Drink 类、一个需要立即食用的 Food 类,以及一个具有特殊存储需求的 Dessert 类——它们都继承菜单项的核心功能。
扩展类
让我们从 Drink 开始实现这些想法:
public class Drink extends MenuItem {
private boolean isCold;
public Drink ( String name, double price, boolean isCold) {
this . name = name;
this . price = price;
this . isCold = isCold;
}
public boolean getIsCold ( ) {
return isCold;
}
public void setIsCold ( boolean isCold) {
this . isCold = isCold;
}
} 技术支持
要定义一个从父类继承的子类,我们在子类名后使用 extends 关键字,随后是父类名。在类定义后,我们定义该子类拥有的任何新属性并实现其构造函数。
但请注意,我们不得不与 isCold 一起重复初始化 name 和 price 。这并不理想,因为父类可能拥有上百个属性。此外,上述代码在编译时会报错,因为这不是初始化父类属性的正确方式。正确方式是使用 super 关键字:
public class Drink extends MenuItem {
private boolean isCold;
public Drink ( String name, double price, boolean isCold) {
super ( name, price) ;
this . isCold = isCold;
}
public boolean getIsCold ( ) {
return isCold;
}
public void setIsCold ( boolean isCold) {
this . isCold = isCold;
}
} 技术支持
super 关键字用于调用父类构造函数。在此示例中, super(name, price) 调用 MenuItem 的构造函数来初始化这些属性,从而避免代码重复。我们只需初始化 Drink 类特有的新属性 isCold 即可。
该关键字非常灵活,您可以在子类的任何部分用它来引用父类。例如,要调用父类方法,使用 super.methodName() ,而 super.attributeName 则用于属性。
方法重写
现在,假设我们想为类添加一个新方法来计算含税总价。由于不同菜单项可能具有不同的税率(例如,现制食品与包装饮料),我们可以使用方法重写,在保持父类方法名一致的同时,在各个子类中实现特定的税费计算。
如下所示:
public class MenuItem {
public double calculateTotalPrice ( ) {
return price * 1.10 ;
}
}
public class Food extends MenuItem {
private boolean isVegetarian;
public Food ( String name, double price, boolean isVegetarian) {
super ( name, price) ;
this . isVegetarian = isVegetarian;
}
@Override
public double calculateTotalPrice ( ) {
return super . getPrice ( ) * 1.15 ;
}
}
public class Drink extends MenuItem {
private boolean isCold;
public Drink ( String name, double price, boolean isCold) {
super ( name, price) ;
this . isCold = isCold;
}
@Override
public double calculateTotalPrice ( ) {
return super . getPrice ( ) * 1.08 ;
}
} 技术支持
在该示例中,方法重写允许每个子类提供其自身对 calculateTotalPrice() 的实现:
基础的 MenuItem 类定义了 10% 的默认税率计算。
当 Food 和 Drink 类扩展 MenuItem 时,它们会重写该方法以实现各自的税率:
食品有更高的 15% 税率
饮品有较低的 8% 税率
@Override 注解用于明确表示这些方法重写了父类的方法。如果方法签名与父类不匹配,它有助于捕获错误。
每个子类仍可通过 super.getPrice() 访问父类的价格,展示了重写方法如何在添加自身行为的同时利用父类功能。
简而言之,方法重写是继承不可或缺的一部分,它允许子类为父类中定义的方法提供自己的实现,在保持相同方法签名的同时实现更具体的行为。
抽象类
我们的 MenuItem 类层次结构已经可用,但有个问题:任何人都应该能够创建一个普通的 MenuItem 对象吗?毕竟,在我们的餐厅里,每个菜单项要么是 Food,要么是 Drink——并不存在所谓“泛化的菜单项”。
我们可以通过将 MenuItem 设为抽象类来阻止这种情况。抽象类只提供基础蓝图——它只能用作继承的父类,不能直接实例化。
要将 MenuItem 设为抽象类,我们在其访问修饰符后添加 abstract 关键字:
public abstract class MenuItem {
private String name;
private double price;
public MenuItem ( String name, double price) {
setName ( name) ;
setPrice ( price) ;
}
public abstract double calculateTotalPrice ( ) ;
} 技术支持
抽象类也可以拥有如上所示的抽象方法 calculateTotalPrice() 。这些抽象方法充当契约,强制子类提供其实现。换言之,抽象类中的任何抽象方法都必须由子类实现。
因此,让我们在这些更改的基础上重写 Food 和 Drink :
public class Food extends MenuItem {
private boolean isVegetarian;
public Food ( String name, double price, boolean isVegetarian) {
super ( name, price) ;
this . isVegetarian = isVegetarian;
}
@Override
public double calculateTotalPrice ( ) {
return getPrice ( ) * 1.15 ;
}
}
public class Drink extends MenuItem {
private boolean hasCaffeine;
public Drink ( String name, double price, boolean hasCaffeine) {
super ( name, price) ;
this . hasCaffeine = hasCaffeine;
}
@Override
public double calculateTotalPrice ( ) {
return getPrice ( ) * 1.10 ;
}
} 技术支持
通过该菜单系统的实现,我们看到了抽象与继承如何协同工作,从而创建灵活、可维护、能轻松适应不同业务需求的代码。
结语
今天,我们一窥 Java 作为面向对象编程语言的能力。我们借助餐厅菜单系统,介绍了类、对象,以及 OOP 的几个关键支柱:封装、继承与抽象。
要让该系统达到生产可用的程度,您仍有许多内容需要学习,例如接口(抽象的一部分)、多态以及面向对象设计模式。想进一步了解这些概念,请参阅我们的 Introduction to OOP in Java 课程。
如果您希望检验自己的 Java 知识,不妨试试回答 我们的 Java 面试题文章 中的部分问题。
Java 始终位列全球最受欢迎的三大编程语言之一。它在企业软件开发、Android 移动应用以及大规模 Web 应用等领域的采用无出其右。其强大的类型系统、庞大的生态以及“一次编写,到处运行”的能力,使其在构建健壮、可扩展系统方面尤为吸引人。本文将探讨 Java 的面向对象编程特性如何帮助开发者有效发挥这些优势,通过合理的代码组织与复用,构建可维护、可扩展的应用程序。
关于组织与运行 Java 代码的说明
在开始写代码之前,先进行一些准备工作。
与语法一样,Java 对代码组织也有严格规定。
首先,每个 public 类必须位于其各自的文件中,文件名与类名完全一致,并以
.java为扩展名。因此,如果我要写一个 Laptop 类,文件名必须是Laptop.java—区分大小写。您可以在同一文件中包含非 public 的类,但最好将它们分开。我知道我们有点超前——在编写类之前就讨论类的组织——但事先大致了解各部分应放在哪里是个好主意。所有 Java 项目都必须有一个包含
Main类的Main.java文件。这里是通过创建对象来测试各个类的地方。要运行 Java 代码,我们将使用 IntelliJ IDEA,这是一款流行的 Java IDE。安装 IntelliJ 后:
Main.java文件,并粘贴以下内容:每当我们讨论类时,代码写在除
Main.java之外的文件里。但如果我们讨论创建和测试对象,就切换到Main.java。要运行程序,您可以单击 main 方法旁边的绿色播放按钮:
输出将显示在底部的 Run 工具窗口中。
如果您完全是 Java 新手,请先查看我们的 Introduction to Java 课程,在继续之前了解 Java 的数据类型和控制流基础。
否则,我们直接开始。
Java 类与对象
那么,类究竟是什么?
类是 Java 中用于表示现实世界概念的编程构造。举例来说,考虑这个 MenuItem 类(在 IDE 中创建文件来编写该类):
该类为我们提供了一个蓝图或模板,用于表示餐厅中的各种菜单项。通过更改类的两个属性
name和price,我们可以创建无数菜单对象,例如汉堡或沙拉。因此,要在 Java 中创建一个类,您需要先写一行用于描述类访问级别的代码(
private、public或protected),紧随其后的是类名。随后在大括号内概述类的各个属性。但我们如何创建属于该类的对象呢?Java 通过构造方法来实现这一点:
构造函数是在我们从类创建新对象时被调用的特殊方法。它会使用我们提供的值来初始化对象的属性。在上面的示例中,构造函数接收名称与价格参数,并使用 “this” 关键字将它们赋给未来对象实例的字段。
构造函数的语法不同于其他类方法,因为它不需要指定返回类型。此外,构造函数的名称必须与类名相同,并且其参数应与您在类定义后声明的属性数量相对应。上面的构造函数创建了两个属性,因为我们在类定义后声明了两个:
name和price。完成类及其构造函数的编写后,您可以在 main 方法中创建该类的实例(对象):
输出:
上面我们创建了两个
MenuItem对象并赋给变量burger和salad。按照 Java 的要求,变量类型必须声明,这里就是MenuItem。随后,为了创建类的实例,我们写上 new 关键字并调用构造方法。除了构造函数,您还可以创建常规方法来赋予类以行为。例如,下面我们添加一个方法来计算含税总价:
现在我们可以计算含税总价:
输出:
封装
类的目的在于为创建对象提供蓝图。这些对象随后会被其他脚本或程序使用。例如,我们的
MenuItem对象可以被一个用户界面使用,在屏幕上展示它们的名称、价格和图片。因此,我们必须以一种只能按预期方式使用其实例的方式来设计类。现在我们的
MenuItem类非常基础且容易出错。有人可能会创建一些荒谬的属性,比如负价格的苹果派或价值百万美元的三明治:所以,编写完类之后的首要任务是通过限制属性的创建与访问方式来保护它们。首先,我们只允许 price 为正值,并设置一个上限,避免意外显示离谱的高价。
Java 允许我们通过 setter 方法来实现:
让我们来看看上面代码块中的新内容:
1. 我们通过添加
private关键字将属性设为私有。这意味着它们只能在 MenuItem 类内部访问。封装始于这一步关键操作。2. 我们新增了一个常量
MAX_PRICE,它:3. 我们添加了
setPrice()方法,它:4. 我们修改了构造函数,使用
setPrice()而不是直接赋值价格。这确保在创建对象时进行价格校验。我们刚刚实现了优良面向对象设计的核心支柱之一——封装。该范式强制进行数据隐藏和对对象属性的受控访问,确保内部实现细节免受外部干扰,只能通过定义良好的接口进行修改。
让我们通过将封装应用到 name 属性上来进一步强调这一点。想象我们有一家咖啡店,只提供拿铁、卡布奇诺、浓缩、美式和摩卡。
因此,我们的菜单项名称只能是该列表中的某一个。以下是如何在代码中强制执行:
上述代码实现了咖啡店菜单项名称的校验。我们来分解一下:
1. 首先,它定义了一个私有的静态常量数组
VALID_NAMES,其中包含唯一允许的饮品名称:latte、cappuccino、espresso、americano 和 mocha。该数组:2. 它声明了一个私有的 String 类型 name 字段用于存储饮品名称
3.
setName()方法实现校验逻辑:VALID_NAMES数组以下是目前完整的类:
在保护了属性的创建方式之后,我们也要保护它们的访问方式。可以通过 getter 方法来实现:
Getter 方法为类的私有属性提供受控访问。它们解决了直接访问属性可能导致非预期修改、破坏封装的问题。
例如,没有 getter 时,我们可能会直接访问属性:
使用 getter 时,我们可强制正确访问:
这种封装:
继承
我们的类开始变得不错,但它仍存在不少问题。比如对于一家提供多种菜品和饮品的大型餐厅来说,这个类不够灵活。
如果我们想添加不同类型的食物项,会遇到诸多挑战。有些菜品可以打包带走,有些需要立即食用。菜单项可能有不同价格和折扣。菜品可能需要温度监控或特殊存储。饮品可冷可热并支持自定义配料。条目可能需要过敏原信息与分量选项。当前系统无法处理这些多样化需求。
继承为这些问题提供了优雅的解决方案。它允许我们通过定义一个包含通用属性的基础
MenuItem类,然后创建继承这些基础并添加独特特性的子类,来构建菜单项的专门版本。例如,我们可以有一个包含温度选项的
Drink类、一个需要立即食用的Food类,以及一个具有特殊存储需求的Dessert类——它们都继承菜单项的核心功能。扩展类
让我们从
Drink开始实现这些想法:要定义一个从父类继承的子类,我们在子类名后使用
extends关键字,随后是父类名。在类定义后,我们定义该子类拥有的任何新属性并实现其构造函数。但请注意,我们不得不与
isCold一起重复初始化name和price。这并不理想,因为父类可能拥有上百个属性。此外,上述代码在编译时会报错,因为这不是初始化父类属性的正确方式。正确方式是使用super关键字:super关键字用于调用父类构造函数。在此示例中,super(name, price)调用MenuItem的构造函数来初始化这些属性,从而避免代码重复。我们只需初始化Drink类特有的新属性isCold即可。该关键字非常灵活,您可以在子类的任何部分用它来引用父类。例如,要调用父类方法,使用
super.methodName(),而super.attributeName则用于属性。方法重写
现在,假设我们想为类添加一个新方法来计算含税总价。由于不同菜单项可能具有不同的税率(例如,现制食品与包装饮料),我们可以使用方法重写,在保持父类方法名一致的同时,在各个子类中实现特定的税费计算。
如下所示:
在该示例中,方法重写允许每个子类提供其自身对
calculateTotalPrice()的实现:基础的
MenuItem类定义了 10% 的默认税率计算。当
Food和Drink类扩展MenuItem时,它们会重写该方法以实现各自的税率:@Override注解用于明确表示这些方法重写了父类的方法。如果方法签名与父类不匹配,它有助于捕获错误。每个子类仍可通过
super.getPrice()访问父类的价格,展示了重写方法如何在添加自身行为的同时利用父类功能。简而言之,方法重写是继承不可或缺的一部分,它允许子类为父类中定义的方法提供自己的实现,在保持相同方法签名的同时实现更具体的行为。
抽象类
我们的
MenuItem类层次结构已经可用,但有个问题:任何人都应该能够创建一个普通的MenuItem对象吗?毕竟,在我们的餐厅里,每个菜单项要么是 Food,要么是 Drink——并不存在所谓“泛化的菜单项”。我们可以通过将
MenuItem设为抽象类来阻止这种情况。抽象类只提供基础蓝图——它只能用作继承的父类,不能直接实例化。要将
MenuItem设为抽象类,我们在其访问修饰符后添加abstract关键字:抽象类也可以拥有如上所示的抽象方法
calculateTotalPrice()。这些抽象方法充当契约,强制子类提供其实现。换言之,抽象类中的任何抽象方法都必须由子类实现。因此,让我们在这些更改的基础上重写
Food和Drink:通过该菜单系统的实现,我们看到了抽象与继承如何协同工作,从而创建灵活、可维护、能轻松适应不同业务需求的代码。
结语
今天,我们一窥 Java 作为面向对象编程语言的能力。我们借助餐厅菜单系统,介绍了类、对象,以及 OOP 的几个关键支柱:封装、继承与抽象。
要让该系统达到生产可用的程度,您仍有许多内容需要学习,例如接口(抽象的一部分)、多态以及面向对象设计模式。想进一步了解这些概念,请参阅我们的 Introduction to OOP in Java 课程。
如果您希望检验自己的 Java 知识,不妨试试回答我们的 Java 面试题文章中的部分问题。