Java 面向对象编程
最后修改于 2024 年 1 月 27 日
在本文中,我们将介绍 Java 中的面向对象编程。我们将提到 Java 对象、对象属性和方法、对象构造函数和访问修饰符。此外,我们还将讨论 super 关键字、构造函数链接、类常量、继承、多态、final 类和私有构造函数。
有三种广泛使用的编程范例:过程式编程、函数式编程和面向对象编程。 Java 主要是一种面向对象的编程语言。自 Java 8 以来,它也支持一些函数式编程。
面向对象编程
面向对象编程 (OOP) 是一种使用对象及其交互来设计应用程序和计算机程序的编程范例。
以下是 OOP 中的基本编程概念
- 抽象
- 多态
- 封装
- 继承
抽象是通过对问题建模合适的类来简化复杂的现实。多态是以不同的方式对不同的数据输入使用运算符或函数的过程。封装向其他对象隐藏类的实现细节。继承是使用已经定义的类来形成新类的一种方式。
Java 对象
对象是 Java OOP 程序的基本构建块。一个对象是数据和方法的组合。在 OOP 程序中,我们创建对象。这些对象通过方法相互通信。每个对象都可以接收消息、发送消息和处理数据。
创建对象有两个步骤。首先,我们定义一个类。类是对象的模板。它是一个蓝图,描述了类的对象共享的状态和行为。一个类可以用来创建多个对象。在运行时从类创建的对象称为该特定类的实例。
package com.zetcode; class Being {} public class SimpleObject { public static void main(String[] args) { Being b = new Being(); System.out.println(b); } }
在我们的第一个例子中,我们创建一个简单的对象。
class Being {}
这是一个简单的类定义。模板的主体是空的。它没有任何数据或方法。
Being b = new Being();
我们创建一个 `Being` 类的新实例。为此,我们有 `new` 关键字。 `b` 变量是创建对象的句柄。
System.out.println(b);
我们将对象打印到控制台以获得对象的一些基本描述。打印对象是什么意思?当我们打印一个对象时,实际上是在调用它的 `toString` 方法。但我们还没有定义任何方法。这是因为每个创建的对象都继承自基类 `Object`。它具有一些所有创建的对象之间共享的基本功能。其中之一是 `toString` 方法。
$ javac com/zetcode/SimpleObject.java $ ls com/zetcode/ Being.class SimpleObject.class SimpleObject.java
编译器创建两个类文件。 `SimpleObject.class` 是应用程序类,`Being.class` 是我们在应用程序中使用的自定义类。
$ java com.zetcode.SimpleObject com.zetcode.Being@125ee71
我们获得对象所属类的名称、@ 字符以及对象的哈希码的无符号十六进制表示。
Java 对象属性
对象属性是捆绑在类的实例中的数据。对象属性称为实例变量或成员字段。实例变量是在类中定义的变量,类的每个对象都有一个单独的副本。
package com.zetcode; class Person { public String name; } public class ObjectAttributes { public static void main(String[] args) { Person p1 = new Person(); p1.name = "Jane"; Person p2 = new Person(); p2.name = "Beky"; System.out.println(p1.name); System.out.println(p2.name); } }
在上面的 Java 代码中,我们有一个 `Person` 类,其中包含一个成员字段。
class Person { public String name; }
我们声明一个 name 成员字段。 `public` 关键字指定成员字段可以在类块外部访问。
Person p1 = new Person(); p1.name = "Jane";
我们创建 `Person` 类的一个实例,并将 name 变量设置为“Jane”。我们使用点运算符来访问对象的属性。
Person p2 = new Person(); p2.name = "Beky";
我们创建 Person
类的另一个实例。在这里,我们将变量设置为“Beky”。
System.out.println(p1.name); System.out.println(p2.name);
我们将变量的内容打印到控制台。
$ java com.zetcode.ObjectAttributes Jane Beky
我们看到程序的输出。`Person` 类的每个实例都有一个单独的 name 成员字段的副本。
Java 方法
方法是在类体内部定义的函数。它们用于对对象的属性执行操作。方法为我们的程序带来模块化。
方法在 OOP 范例的封装概念中至关重要。例如,我们的 `AccessDatabase` 类中可能有一个 `connect` 方法。我们不需要了解 `connect` 方法如何连接到数据库。我们只需要知道它用于连接到数据库。这对于在编程中划分职责至关重要,尤其是在大型应用程序中。
对象将状态和行为分组在一起。方法代表对象的行为部分。
package com.zetcode; class Circle { private int radius; public void setRadius(int radius) { this.radius = radius; } public double area() { return this.radius * this.radius * Math.PI; } } public class Methods { public static void main(String[] args) { Circle c = new Circle(); c.setRadius(5); System.out.println(c.area()); } }
在代码示例中,我们有一个 `Circle` 类。在该类中,我们定义了两个方法。 `setRadius` 方法将一个值分配给 `radius` 成员,`area` 方法从类成员和一个常量计算圆的面积。
private int radius;
我们在类中有一个成员字段。它是圆的半径。 `private` 关键字是一个访问说明符。它表示该变量被限制在外部世界。如果我们要从外部修改此变量,我们必须使用公开可用的 `setRadius` 方法。这样我们就可以保护我们的数据。
public void setRadius(int radius) { this.radius = radius; }
这是 `setRadius` 方法。 `this` 变量是一个特殊变量,我们用它来从方法访问成员字段。 `this.radius` 是一个实例变量,而 `radius` 是一个局部变量,仅在 `setRadius` 方法内部有效。
Circle c = new Circle(); c.setRadius(5);
我们创建 `Circle` 类的一个实例,并通过在圆的对象上调用 `setRadius` 方法来设置其半径。点运算符用于调用该方法。
public double area() { return this.radius * this.radius * Math.PI; }
`area` 方法返回圆的面积。`Math.PI` 是一个内置常量。
$ java com.zetcode.Methods 78.53981633974483
Java 访问修饰符
访问修饰符设置方法和成员字段的可见性。 Java 有三个访问修饰符:`public`、`protected` 和 `private`。 `public` 成员可以从任何地方访问。
`protected` 成员只能在类本身、继承的类以及来自同一包的其他类中访问。最后,`private` 成员仅限于包含类型,例如仅在其类或接口中。如果我们没有指定访问修饰符,我们将具有包私有可见性。在这种情况下,成员和方法可以在同一个包中访问。
访问修饰符保护数据免受意外修改。 它们使程序更加健壮。
类别 | 包 | 子类(同一包) | 子类(其他包) | 世界 | |
---|---|---|---|---|---|
公共 | + | + | + | + | + |
受保护的 | + | + | + | + | o |
无修饰符 | + | + | + | o | o |
私人的 | + | o | o | o | o |
上表总结了 Java 访问修饰符(+ 可访问,o 不可访问)。
package com.zetcode; class Person { public String name; private int age; public int getAge() { return this.age; } public void setAge(int age) { this.age = age; } } public class AccessModifiers { public static void main(String[] args) { Person p = new Person(); p.name = "Jane"; p.setAge(17); System.out.println(String.format("%s is %d years old", p.name, p.getAge())); } }
在上面的程序中,我们有两个成员字段:public 和 private。
public int getAge() { return this.age; }
如果成员字段是私有的,则访问它的唯一方法是通过方法。如果我们要修改类外部的属性,则必须将该方法声明为 `public`。这是数据保护的一个重要方面。
public void setAge(int age) { this.age = age; }
`setAge` 方法使我们能够从类定义外部更改私有 `age` 变量。
Person p = new Person(); p.name = "Jane";
我们创建 `Person` 类的一个新实例。因为 `name` 属性是 `public`,所以我们可以直接访问它。但是,不建议这样做。
p.setAge(17);
`setAge` 方法修改 `age` 成员字段。它无法直接访问或修改,因为它被声明为 `private`。
System.out.println(String.format("%s is %d years old", p.name, p.getAge()));
最后,我们访问这两个成员以构建一个字符串,该字符串将打印到控制台。
$ java com.zetcode.AccessModifiers Jane is 17 years old
运行该示例,我们得到以下输出。
以下程序显示了访问修饰符如何影响子类继承成员的方式。
package com.zetcode; class Base { public String name = "Base"; protected int id = 5323; private boolean isDefined = true; } class Derived extends Base { public void info() { System.out.println("This is Derived class"); System.out.println("Members inherited:"); System.out.println(this.name); System.out.println(this.id); // System.out.println(this.isDefined); } } public class ProtectedMember { public static void main(String[] args) { Derived drv = new Derived(); drv.info(); } }
在此程序中,我们有一个 `Derived` 类,该类继承自 `Base` 类。 `Base` 类有三个成员字段,所有成员字段都具有不同的访问修饰符。 `isDefined` 成员未被继承。 `private` 修饰符阻止了这一点。
class Derived extends Base {
`Derived` 类继承自 `Base` 类。要从另一个类继承,我们使用 `extends` 关键字。
System.out.println(this.name); System.out.println(this.id); // System.out.println(this.isDefined);
`public` 和 `protected` 成员由 `Derived` 类继承。他们可以被访问。 `private` 成员未被继承。访问成员字段的行被注释掉了。如果我们取消注释该行,代码将无法编译。
$ java com.zetcode.ProtectedMember This is Derived class Members inherited: Base 5323
运行该程序,我们收到以下输出。
Java 构造函数
构造函数是一种特殊的方法。创建对象时会自动调用它。构造函数不返回值,也不使用 `void` 关键字。构造函数的目的是启动对象的状态。构造函数与类具有相同的名称。构造函数是方法,因此它们也可以被重载。构造函数不能直接调用。 `new` 关键字调用它们。构造函数不能声明为 synchronized、final、abstract、native 或 static。
构造函数不能被继承。它们按照继承的顺序调用。如果我们没有为类编写任何构造函数,Java 会提供一个隐式默认构造函数。如果我们提供任何类型的构造函数,则不会提供默认构造函数。
package com.zetcode; class Being { public Being() { System.out.println("Being is created"); } public Being(String being) { System.out.println(String.format("Being %s is created", being)); } } public class Constructor { @SuppressWarnings("ResultOfObjectAllocationIgnored") public static void main(String[] args) { new Being(); new Being("Tom"); } }
我们有一个 Being 类。这个类有两个构造函数。第一个不带参数,第二个带一个参数。
public Being() { System.out.println("Being is created"); }
此构造函数不带任何参数。
public Being(String being) { System.out.println(String.format("Being %s is created", being)); }
此构造函数采用一个字符串参数。
@SuppressWarnings("ResultOfObjectAllocationIgnored")
此注释将阻止警告,即我们不会将创建的对象分配给任何变量。通常这是一种可疑的活动。
new Being();
创建 `Being` 类的一个实例。在对象创建时调用无参数构造函数。
new Being("Tom");
创建 `Being` 类的另一个实例。这次在对象创建时调用带参数的构造函数。
$ java com.zetcode.Constructor Being is created Being Tom is created
在下一个例子中,我们初始化该类的数据成员。初始化变量是构造函数的典型工作。
package com.zetcode; class User { private String occupation; private String name; public User(String name, String occupation) { this.name = name; this.occupation = occupation; } public void info() { System.out.format("%s is a %s\n", this.name, this.occupation); } } public class MemberInit { public static void main(String[] args) { String name = "John Doe"; String occupation = "gardener"; User u = new User(name, occupation); u.info(); } }
我们有一个具有数据成员和方法的 `User` 类。
private String occupation; private String name;
我们在类定义中有两个私有变量。
public User(String name, String occupation) { this.name = name; this.occupation = occupation; }
在构造函数中,我们初始化两个数据成员。 `this` 关键字是一个处理程序,用于从方法引用对象变量。当构造函数参数的名称与成员的名称相等时,必须使用 `this` 关键字。否则,该用法是可选的。
User u = new User(name, occupation); u.info();
我们创建一个带有两个参数的 `MyFriend` 对象。然后我们调用对象的 `info` 方法。
$ java com.zetcode.MemberInit John Doe is a gardener
Java super 关键字
`super` 关键字是一个引用变量,在子类中使用它来引用直接父类对象。它可用于引用父类的 a) 实例变量,b) 构造函数,c) 方法。
package com.zetcode; class Shape { int x = 50; int y = 50; } class Rectangle extends Shape { int x = 100; int y = 100; public void info() { System.out.println(x); System.out.println(super.x); } } public class SuperVariable { public static void main(String[] args) { Rectangle r = new Rectangle(); r.info(); } }
在该示例中,我们使用 `super` 关键字引用父类的变量。
public void info() { System.out.println(x); System.out.println(super.x); }
在 `info` 方法中,我们使用 `super.x` 语法引用父类的实例变量。
如果构造函数没有显式调用超类构造函数,Java 会自动插入对超类的无参数构造函数的调用。如果超类没有无参数构造函数,我们会收到编译时错误。
package com.zetcode; class Vehicle { public Vehicle() { System.out.println("Vehicle created"); } } class Bike extends Vehicle { public Bike() { // super(); System.out.println("Bike created"); } } public class ImplicitSuper { public static void main(String[] args) { Bike bike = new Bike(); System.out.println(bike); } }
该示例演示了对父类构造函数的隐式调用。
public Bike() { // super(); System.out.println("Bike created"); }
如果我们取消注释该行,我们会得到相同的结果。
$ java com.zetcode.ImplicitSuper Vehicle created Bike created com.zetcode.Bike@15db9742
创建 `Bike` 对象时会调用两个构造函数。
一个类中可以有多个构造函数。
package com.zetcode; class Vehicle { protected double price; public Vehicle() { System.out.println("Vehicle created"); } public Vehicle(double price) { this.price = price; System.out.printf("Vehicle created, price %.2f set%n", price); } } class Bike extends Vehicle { public Bike() { super(); System.out.println("Bike created"); } public Bike(double price) { super(price); System.out.printf("Bike created, its price is: %.2f %n", price); } } public class SuperCalls { public static void main(String[] args) { Bike bike1 = new Bike(); Bike bike2 = new Bike(45.90); } }
该示例使用 `super` 的不同语法来调用不同的父类构造函数。
super();
在这里,我们调用父类的无参数构造函数。
super(price);
此语法调用父类的构造函数,该构造函数采用一个参数:自行车的价格。
$ java com.zetcode.SuperCalls Vehicle created Bike created Vehicle created, price 45.90 set Bike created, its price is: 45.90
Java 构造函数链接
构造函数链接是从构造函数调用另一个构造函数的能力。要从同一个类调用另一个构造函数,我们使用 `this` 关键字。要从父类调用另一个构造函数,我们使用 `super` 关键字。
package com.zetcode; class Shape { private int x; private int y; public Shape(int x, int y) { this.x = x; this.y = y; } protected int getX() { return this.x; } protected int getY() { return this.y; } } class Circle extends Shape { private int r; public Circle(int r, int x, int y) { super(x, y); this.r = r; } public Circle() { this(1, 1, 1); } @Override public String toString() { return String.format("Circle: r:%d, x:%d, y:%d", r, getX(), getY()); } } public class ConstructorChaining { public static void main(String[] args) { Circle c1 = new Circle(5, 10, 10); Circle c2 = new Circle(); System.out.println(c1); System.out.println(c2); } }
我们有一个 `Circle` 类。该类有两个构造函数。一个采用一个参数,另一个不带任何参数。
class Shape { private int x; private int y; ... }
`Shape` 类负责处理各种形状的 `x` 和 `y` 坐标。
public Shape(int x, int y) { this.x = x; this.y = y; }
`Shape` 类的构造函数使用给定的参数初始化 `x` 和 `y` 坐标。
protected int getX() { return this.x; } protected int getY() { return this.y; }
我们定义了两个方法来检索坐标的值。这些成员是私有的,因此唯一可能的访问是通过方法。
class Circle extends Shape { private int r; ... }
`Circle` 类继承自 `Shape` 类。它定义了特定于此形状的 `radius` 成员。
public Circle(int r, int x, int y) { super(x, y); this.r = r; }
`Circle` 类的第一个构造函数采用三个参数:`radius`、`x` 和 `y` 坐标。使用 `super` 关键字,我们调用父类的构造函数,传递坐标。请注意,`super` 关键字必须是构造函数中的第一个语句。第二个语句初始化 `Circle` 类的 `radius` 成员。
public Circle() { this(1, 1, 1); }
第二个构造函数不带任何参数。在这种情况下,我们提供一些默认值。 `this` 关键字用于调用同一类的三参数构造函数,传递三个默认值。
@Override public String toString() { return String.format("Circle: r:%d, x:%d, y:%d", r, getX(), getY()); }
在 `toString` 方法中,我们提供 `Circle` 类的字符串表示形式。要确定 `x` 和 `y` 坐标,我们使用继承的 `getX` 和 `getY` 方法。
$ java com.zetcode.ConstructorChaining Circle: r:5, x:10, y:10 Circle: r:1, x:1, y:1
Java 类常量
可以创建类常量。这些常量不属于具体的对象。它们属于该类。按照惯例,常量用大写字母书写。
package com.zetcode; class Math { public static final double PI = 3.14159265359; } public class ClassConstant { public static void main(String[] args) { System.out.println(Math.PI); } }
我们有一个带有 PI
常量的 Math
类。
public static final double PI = 3.14159265359;
`final` 关键字用于定义常量。 `static` 关键字使无需创建类的实例即可引用成员。 `public` 关键字使其可以在类体外部访问。
$ java com.zetcode.ClassConstant 3.14159265359
运行该示例,我们得到上面的输出。
Java toString 方法
每个对象都有 `toString` 方法。它返回对象的易于理解的表示形式。默认实现返回 `Object` 类型的完全限定名称。当我们使用对象作为参数调用 `System.out.println` 方法时,将调用 `toString`。
package com.zetcode; class Being { @Override public String toString() { return "This is Being class"; } } public class ThetoStringMethod { public static void main(String[] args) { Being b = new Being(); Object o = new Object(); System.out.println(o.toString()); System.out.println(b.toString()); System.out.println(b); } }
我们有一个 `Being` 类,我们覆盖了 `toString` 方法的默认实现。
@Override public String toString() { return "This is Being class"; }
每个创建的类都继承自基类 `Object`。 `toString` 方法属于此对象类。 `@Override` 注释通知编译器该元素旨在覆盖超类中声明的元素。然后,编译器将检查我们是否未创建任何错误。
Being b = new Being(); Object o = new Object();
我们创建两个对象:一个自定义定义的对象和一个内置对象。
System.out.println(o.toString()); System.out.println(b.toString());
我们显式地在两个对象上调用 `toString` 方法。
System.out.println(b);
正如我们之前指定的,将对象作为参数传递给 `System.out.println` 将调用其 `toString` 方法。这次,我们隐式地调用了该方法。
$ java com.zetcode.ThetoStringMethod java.lang.Object@125ee71 This is Being class This is Being class
这是我们运行该示例时得到的结果。
Java 中的继承
继承 (Inheritance) 是一种使用已定义的类来创建新类的方法。 新创建的类称为派生 (derived) 类,我们从中派生的类称为基 (base) 类。 继承的重要优点是代码重用和降低程序的复杂性。 派生类(子类)覆盖或扩展基类(父类)的功能。
package com.zetcode; class Being { public Being() { System.out.println("Being is created"); } } class Human extends Being { public Human() { System.out.println("Human is created"); } } public class Inheritance { @SuppressWarnings("ResultOfObjectAllocationIgnored") public static void main(String[] args) { new Human(); } }
在这个程序中,我们有两个类:一个基类 Being
和一个派生类 Human
。 派生类继承自基类。
class Human extends Being {
在 Java 中,我们使用 extends
关键字来创建继承关系。
new Human();
我们实例化派生类 Human
。
$ java com.zetcode.Inheritance Being is created Human is created
我们可以看到两个构造函数都被调用了。首先,调用基类的构造函数,然后调用派生类的构造函数。
接下来是一个更复杂的示例。
package com.zetcode; class Being { static int count = 0; public Being() { count++; System.out.println("Being is created"); } public void getCount() { System.out.format("There are %d Beings%n", count); } } class Human extends Being { public Human() { System.out.println("Human is created"); } } class Animal extends Being { public Animal() { System.out.println("Animal is created"); } } class Dog extends Animal { public Dog() { System.out.println("Dog is created"); } } public class Inheritance2 { @SuppressWarnings("ResultOfObjectAllocationIgnored") public static void main(String[] args) { new Human(); Dog dog = new Dog(); dog.getCount(); } }
有四个类时,继承层次结构会更复杂。 Human
和 Animal
类继承自 Being
类,而 Dog
类直接继承自 Animal
类,间接继承自 Being
类。
static int count = 0;
我们定义一个 static
变量。 静态成员由类的所有实例共享。
public Being() { count++; System.out.println("Being is created"); }
每次实例化 Being
类时,我们将 count 变量增加 1。 这样,我们就可以跟踪已创建的实例数量。
class Animal extends Being { ... class Dog extends Animal { ...
Animal
继承自 Being
,而 Dog
继承自 Animal
。 间接地,Dog
也继承自 Being
。
new Human(); Dog dog = new Dog(); dog.getCount();
我们从 Human
和 Dog
类创建实例。 我们调用 Dog
对象的 getCount
方法。
$ java com.zetcode.Inheritance2 Being is created Human is created Being is created Animal is created Dog is created There are 2 Beings
Human
对象调用两个构造函数。 Dog
对象调用三个构造函数。 有两个 Beings
被实例化。
Java 多态
多态 (Polymorphism) 是对不同的数据输入以不同的方式使用运算符或函数的过程。 实际上,多态意味着如果类 B 继承自类 A,它不必继承类 A 的所有内容;它可以以不同的方式执行类 A 的某些操作。
通常,多态是以不同形式出现的能力。 从技术上讲,它是为派生类重新定义方法的能力。 多态与将特定实现应用于接口或更通用的基类有关。
简而言之,多态是为派生类重新定义方法的能力。
package com.zetcode; abstract class Shape { protected int x; protected int y; public abstract int area(); } class Rectangle extends Shape { public Rectangle(int x, int y) { this.x = x; this.y = y; } @Override public int area() { return this.x * this.y; } } class Square extends Shape { public Square(int x) { this.x = x; } @Override public int area() { return this.x * this.x; } } public class Polymorphism { public static void main(String[] args) { Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) }; for (Shape shape : shapes) { System.out.println(shape.area()); } } }
在上面的程序中,我们有一个抽象 Shape
类。 这个类变形为两个子类:Rectangle
和 Square
。 两者都提供了 area
方法的自己的实现。 多态为 OOP 系统带来了灵活性和可伸缩性。
@Override public int area() { return this.x * this.y; } ... @Override public int area() { return this.x * this.x; }
Rectangle
和 Square
类具有 area
方法的自己的实现。
Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) };
我们创建一个包含三个形状的数组。
for (Shape shape : shapes) { System.out.println(shape.area()); }
我们遍历每个形状,并对其调用 area
方法。 编译器为每个形状调用正确的方法。 这就是多态的本质。
最终类,私有构造函数
具有 final
修饰符的类不能被继承。 具有 private
修饰符的构造函数的类不能被实例化。
package com.zetcode; final class MyMath { public static final double PI = 3.14159265358979323846; // other static members and methods } public class FinalClass { public static void main(String[] args) { System.out.println(MyMath.PI); } }
我们有一个 MyMath
类。 此类具有一些静态成员和方法。 我们不希望任何人从我们的类继承;因此,我们将其声明为 final
。
此外,我们也不希望允许从我们的类创建实例。 我们决定仅从静态上下文中使用它。 通过声明一个私有构造函数,该类不能被实例化。
package com.zetcode; final class MyMath { private MyMath() {} public static final double PI = 3.14159265358979323846; // other static members and methods } public class PrivateConstructor { public static void main(String[] args) { System.out.println(MyMath.PI); } }
我们的 MyMath
类不能被实例化,也不能被继承。 这就是 Java 语言中 java.lang.Math
的设计方式。
来源
在本文中,我们介绍了 Java 中的面向对象编程。
作者
列出所有Java教程。