面向对象程序设计(object-oriented programming,OOP)是当今主流的程序设计范型,由于Java是面向对象的,所以必须熟悉OOP才能很好的使用Java。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
传统的结构化程序设计通过设计一系列的过程(算法)来求解问题,而OOP却调换了这个顺序,将数据放在第一位,然后再考虑操作数据的算法。
对于一些规模较小的问题,将其分解为过程的开发方式比较理想。面向对象更加适合解决规模较大的问题。
类(class)是构造对象的模板或蓝图,由类构造(construct)对象的过程称为创建类的实例(instance)。
封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。作为一个类的实例,特定对象都有一组特
定的实例字段值。这些值的集合就是这个对象的当前状态(state)。
实现封装的关键在于,绝不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互(类似黑盒)。
在Java中,所有的类都源自一个“超类”,它就是Object,所有的类都扩展自这个类。
通过扩展一个类来建立另一个类的过程称为继承(inheritance),有关信息会在后续文章中介绍。
我们对象的三个主要特性:
同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法来定义的。
每个对象都保存着描述当前状态的信息(状态)。对象状态的改变必须通过调用方法来实现(封装性)。
对象的状态并不能完全描述一个对象。每个对象都有一个唯一的标识(identity)。
对象的这些关键特性会彼此相互影响。
识别类的一个简单经验是在分析问题的过程中寻找名词,而方法对应着动词。
在类中,最常见的关系有:
依赖是一种最明显的、最常见的关系。如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。应该尽可能地将相互依赖地类减至最少。
聚合即包含关系。
继承表示一个更特殊的类与一个更一般的类之间的关系。
在Java中,要使用构造器(constructor,或称构造函数)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
构造器的名字应该与类名相同。要想构造一个类对象,需要在构造器前面加上new操作符:
new Date();
如果需要的话,也可以将这个对象传递给一个方法:
System.out.println(new Date());
或者也可以对刚刚创建的对象应用一个方法:
String s = new Date();
要注意对象与对象变量之间的区分,对象变量不是一个对象,而它实际上也没有引用任何对象,必须对其进行初始化。有两个选择,一是初始化这个变量,让它引用一个新构造的对象;二是设置这个变量,让它引用一个已有的对象:
Date deadline = new Date();
deadline = birthday;
访问对象且修改对象的方法称为更改器方法(mutator method);访问对象但不修改对象的方法称为访问器方法(accessor method)。
要想构建一个完整的程序,会结合使用多个类,其中只有一个类有main方法。
在Java中,最简单的类定义形式为:
class ClassName
{
field1;
field2;
...
constructor1;
constructor2;
...
method1;
method2;
...
}
下面看一个非常简单的Employee类。在编写工资管理系统时可能会用到:
class Employee
{
// instance fields
private String name;
private double salary;
private localDate hireDay;
// constructor
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
hireDay = localDate.of(year, month, day);
}
// a method
public String getName()
{
return name;
}
// more method
...
}
在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。
下面给出程序源代码:
import java.time.*;
/**
* This program tests the Employee class.
* @version 1.13 2018-04-10
* @author Cay Horstmann
*/
public class EmployeeTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
// raise everyone's salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay="
+ e.getHireDay());
}
}
class Employee
{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
一个源文件包含多个类,部分程序员喜欢将每一个类存在一个单独的源文件中,如果喜欢这样组织文件,有两种编译源程序的方法。
一是使用通配符调用java编译器:
javac Employee*.java
或者键入以下命令:
javac EmployeeTest.java
Employee类中包含一个构造器和4个方法:
public Employee(String n, double s, int year, int month, int day)
public String getName()
public double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)
所有方法都被标记为public。关键字public意味着任何类的方法都可以调用这些方法。
同时设置了3个实例字段用来存放要操作的数据:
private String name;
private double salary;
private localDate hireDay;
关键字private确保只有Employee类自身才能访问这些字段,而其他类的方法不能读写这些字段。
类包含的实例字段通常属于某个类类型。
Employee类的构造器如下:
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
构造器与类同名,在构造Employee类的对象时,构造器会运行,从而将实例字段初始化为所希望的初始状态。
构造器总是结合new运算符来调用。
以下是构造器的几个特性:
注:不要在构造器中定义与实例字段同名的局部变量。
同时可以使用var声明局部变量:
var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
注意var关键字只能用于方法中的局部变量,参数和字段的类型必须声明。
一个对象变量包含一个对象的引用,或者包含一个特殊值null,后者表示没有引用任何对象。使用null时,必须明确哪些字段可能为null,不然会出错:
localDate birthday = null;
String s = birthday.toString(); // NullPointerException
对此有两种解决方法:
”宽容型“方法是把null参数转换成为一个适当的非null值:
if (n == null) name = "unknown";
else name = n;
在Java9中,Objects类对此提供了一个便利方法:
public Employee(String n, double s, int year, int month, int day)
{
name = Objects.requireNonNullElse(n, "unknown");
...
}
”严格型“方法则是干脆拒绝null参数:
public Employee(String n, double s, int year, int month, int day)
{
Objects requireNonNull(n, "The name can't be null");
name = n;
...
方法用于操作对象以及存取它们的实例字段。如:
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
调用这个方法的对象的salary实例字段设置为一个新值。考虑以下调用:
number07.raiseSalary(5);
调用过程如下:
double raise = number07.salary * 5 / 100;
number07.salary += raise;
raiseSalary方法中有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法明前的Employee类型的对象。第二个参数是位于方法名后括号中的数值,称为显式(explicit)参数。
在每一个方法中,关键字this指示隐式参数,如:
public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
最后再看下getName方法:
public String getName()
{
return name;
}
这是典型的访问器方法。只返回实例字段值,因此又称为字段访问器。这样可以保护name字段不会受到外界的破坏。
有时,可能想要获得或设置实例字段的值,就需要提供三项内容:
如果将一个字段定义为static,每个类只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。如:
class Employee
{
private static int nextId = 1;
private int id;
}
每个Employee对象都有一个自己的id字段,但这个类的所有实例共享一个nextId字段。即使没有Employee对象,静态字段nextId也存在。它属于类,而不属于单个的对象。
静态变量用的较少,但静态常量比较常用。如Math类中定义一个静态常量:
public class Math
{
...
public static final double PI = 3.14159265358979323846;
...
}
在程序中,可以使用Math.PI来访问这个常量。
若省略关键字static,PI就变成Math类的一个实例字段。
每个类对象都可以修改公共字段,所以,最好不要有公共字段。公共常量(即final字段)却没问题。
静态方法是不在对象上执行的方法。如Math类的pow方法:
Math.pow(x,a)
会计算幂 x 2 x^2 x2。在完成运算时,它并不使用任何Math对象,即没有隐式参数。
以下两种情况可以使用静态方法:
静态方法还有另一种常见的用途,类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象。NumberFormat类如下生成不同风格的格式化对象:
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.3;
System.out.println(currencyFormatter.format(x)); // prints $0.10
System.out.println(percentFormatter.format(x)); // prints 10%
并不使用构造器主要是由于:
可以调用静态方法而不需要对象。main方法也是一个静态方法,该方法不对任何对象进行操作。实际上,在启动程序时还没有任何对象。静态的main方法将执行并构造程序所需要的对象。
按值调用(call by value)表示方法接收的是调用者提供的值。而按引用调用(call by reference)表示方法接收的是调用者提供的地址。
Java程序设计语言总是按值调用。一个方法不可能修改基本数据类型的参数,但可以修改对象引用作为的参数。如:
public static void tripleSalary(Employee x) // works
{
x.raiseSalary(200);
}
harry = new Employee(...);
tripleSalary(harry);
实际上,Java对对象采用的也不是引用调用。实际上,对象引用是值传递的。
下面总结下Java中对方法参数能做什么而不能做什么:
可以尝试运行下列程序进行测试:
/**
* This program demonstrates parameter passing in Java.
* @version 1.01 2018-04-10
* @author Cay Horstmann
*/
public class ParamTest
{
public static void main(String[] args)
{
/*
* Test 1: Methods can't modify numeric parameters
*/
System.out.println("Testing tripleValue:");
double percent = 10;
System.out.println("Before: percent=" + percent);
tripleValue(percent);
System.out.println("After: percent=" + percent);
/*
* Test 2: Methods can change the state of object parameters
*/
System.out.println("\nTesting tripleSalary:");
var harry = new Employee("Harry", 50000);
System.out.println("Before: salary=" + harry.getSalary());
tripleSalary(harry);
System.out.println("After: salary=" + harry.getSalary());
/*
* Test 3: Methods can't attach new objects to object parameters
*/
System.out.println("\nTesting swap:");
var a = new Employee("Alice", 70000);
var b = new Employee("Bob", 60000);
System.out.println("Before: a=" + a.getName());
System.out.println("Before: b=" + b.getName());
swap(a, b);
System.out.println("After: a=" + a.getName());
System.out.println("After: b=" + b.getName());
}
public static void tripleValue(double x) // doesn't work
{
x = 3 * x;
System.out.println("End of method: x=" + x);
}
public static void tripleSalary(Employee x) // works
{
x.raiseSalary(200);
System.out.println("End of method: salary=" + x.getSalary());
}
public static void swap(Employee x, Employee y)
{
Employee temp = x;
x = y;
y = temp;
System.out.println("End of method: x=" + x.getName());
System.out.println("End of method: y=" + y.getName());
}
}
class Employee // simplified Employee class
{
private String name;
private double salary;
public Employee(String n, double s)
{
name = n;
salary = s;
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
有些类有多个构造器。如可以如下构造一个空的StringBuilder对象:
var message = new StringBuilder()
或者指定一个初始字符串:
var todoList = new StringBuilder("To do:\n");
这个功能叫做重载(overloading)。如果多个方法有相同的名字、不同的参数,便出现重载。查找匹配的过程称为重载解析(overloading resolution)。
Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指定方法名以及参数类型,这叫做方法的签名(signature)。
如果构造器中没有显式地为字段设置处值,就会被自动地赋为默认值:数值为0,布尔值为false,对象引用为null。
如果一个类没有编写构造器,就会为你提供一个无参构造器。这个构造器将有所得实例字段设为默认值。
如果类中至少提供了一个构造器,但没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的。
通过重载类的构造器方法,可以采用多种形式设置类的实例字段的初始状态。
初始值不一定是常量值。如:
class Employee
{
private static int nextId;
private int id = assignId();
...
private static int assignId()
{
int r = nextId;
nextId++;
return r;
}
...
}
我们通常喜欢用单个字母作为参数名,但这样做只有阅读代码时才能了解参数的含义,有些程序员在每个参数前加上前缀“a”作为参数名。
还有一种常用技巧:参数变量会遮蔽同名的实例字段。如:
public Employee(String name, double salary)
{
this.name = name;
this.salary = salary;
}
关键字this指示一个方法的隐式参数。不过这个关键字还有另一层含义:
如果构造器的第一个语句形如 this(…) ,这个构造器将调用同一个类中的另一个构造器,如:
public Employee(double s)
{
// calls Employee(String, double);
this("Employee #" + nextId, s);
nextId++;
}
这样对公共的构造器代码只需要编写一次即可。
Java还有第三种机制,成为初始化块(initialization block)。在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。如:
class Employee
{
private static int nextId;
private int id;
private String name;
private double salary;
// object initialization block
{
id = nextId;
nextId++;
}
public Empolyee(String n, double s)
{
name = n;
salary = s;
}
public Empolyee()
{
name = "";
salary = 0;
}
...
}
无论调用哪个构造器对象,id字段都会在对象初始化块中初始化,首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必须的,通常会直接将初始化代码放在构造器中。
若在初始化块前加static,则在类第一次加载时,会进行静态字段的初始化。
由于Java会完成自动的垃圾回收,因此不支持析构器。
如果对象使用了内存外的其他资源,则需要进行资源的回收和再利用。
finalize方法目前已被废弃,不作介绍。
Java允许使用包(package)将类组织在一个集合中。
使用包的主要原因是确保类名的唯一性。为了保证包名的绝对唯一性,要用一个因特网名以逆序的形式作为包名,然后对不同的工程使用不同的子包。
一个类可以使用所属包中的所有类,以及其他包中的公共类(public class)。
我们可以采用两种方式访问另一个包中的公共类。第一种方式是使用完全限定名(fully qualified name);就是包名后面跟类名,如:
java.time.localDate today = javat.time.localDate.now()
更常用的是使用import语句。如:`
import java.time.*
当两个包含有相同的类名时,可以增加一个特定的import语句来解决问题:
import java.util.*
import java.sql.*
import java.util.Date;
var deadline = new java.util.Date();
var today = new java.sql.Date(...);
有一种import语句允许导入静态方法和静态字段,如:
import static java.lang.System.*;
就可以使用System类的静态方法和静态字段,而不必加类名前缀:
out.println("Goodbye, World!"); // i.e., System.out
exit(0); // i.e., System.out;
另外可以导入特定的方法或字段:
import static java.lang.System.out;
想要将类放入包中,就必须将包的名字放在源文件的开头:
package com.horstmann.corejava;
public class Employee
{
...
}
标记为public的部分可以由任意类使用,标记为private的部分只能由定义它们的类使用。
类的路径必须和包名匹配。
另外,类文件也可以存储在JAR(Java归档)文档中。
最好使用 -classpath(或 -cp, 或Java9中的 --class-path)选项指定类路径:
java -classpth /home/user/classdir:.:/home/user/archives/archive.jar MyProg
或
java -classpath c:\classdir;.c:\archives\archive.jar Myprog
一个JAR文件可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。此外,JAR文件是压缩的,它使用了我们熟悉的ZIP压缩格式。
可以使用jar工具制作JAR文件,创建命令如下:
jar cvf jarFileName file1 file2 ...
如:
jar cvf CalculationClasses.jar *.class icon.gif
通常jar命令的格式如下:
jar options file1 file2 ...
每个JAR文件还包含一个清单文件(manifest),用于描述归档文件的特殊性。
清单文件被命名为MANIFEST.MF,它位于JAR文件一个特殊的META-INF子目录中。
要创建一个包含清单文件的JAR文件,应执行:
jar cfm jarFileName manifestFileName ...
如:
jar cfm MyArchive.jar manifest.mf com/mycompany/mypkg/*.class
要更新已有的JAR文件的清单,则应使用:
jar ufm MyArchive.jar manifest-additions.mf
可以使用jar命令中的e选项指定程序的入口点,即通常需要在调用java程序启动器时指定的类:
jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass files to add
或者,可以在清单文件中指定程序的主类,包括以下形式的语句:
Main-Class: com.mycompany.mypkg.MainAppClass
无论哪种方法,用户都可以通过以下命令启动程序:
java -jar MyProgram.jar
Java9引入了多版本JAR,可以包含面向不同Java版本的类文件。为保证向后兼容,额外的类文件放在META-INF/versions目录中。
要增加不同版本的类文件,可以使用 --release 标志:
jar uf MyProgram.jar --release 9 Application.class
要从头构建一个多版本JAR文件,可以使用 -C 选项:
jar cf MyProgram.jar -C bin/8 . --release 9 -c bin/9 Application.class
面向不同版本编译时,要使用 --release标志和 -d标志来指定输出目录:
javac -d bin/8 --release 8 ...
JDK包含了一个很有用的工具,叫javadoc,可以有源文件生成一个HTML文档。
javadoc实用工具从下面几项中抽取信息:
每个/**…*/文档注释包含标记以及之后紧跟着的自由格式文本,如:
/**
* A.....
* ...
* ...
*/
public class Card
{
...
}
注释有类注释、方法注释、字段注释、通用注释、包注释等,这里不一一介绍
这里介绍了一些简单的类设计技巧:
狂神说Java
Java核心技术 卷I(第11版)
上一章:Java从零开始系列01:Java入门
下一章:Java从零开始系列03:继承