Java从零开始系列02:对象与类

学习目标:

  • 面向对象程序设计入门
  • 如何创建标准Java类库中的对象
  • 如何编写自己的类

一、面向对象程序设计概述

面向对象程序设计(object-oriented programming,OOP)是当今主流的程序设计范型,由于Java是面向对象的,所以必须熟悉OOP才能很好的使用Java。

面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。

传统的结构化程序设计通过设计一系列的过程(算法)来求解问题,而OOP却调换了这个顺序,将数据放在第一位,然后再考虑操作数据的算法。

对于一些规模较小的问题,将其分解为过程的开发方式比较理想。面向对象更加适合解决规模较大的问题。

(一)类

类(class)是构造对象的模板或蓝图,由类构造(construct)对象的过程称为创建类的实例(instance)。

封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。作为一个类的实例,特定对象都有一组特
定的实例字段值。这些值的集合就是这个对象的当前状态(state)。

实现封装的关键在于,绝不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互(类似黑盒)。

在Java中,所有的类都源自一个“超类”,它就是Object,所有的类都扩展自这个类。

通过扩展一个类来建立另一个类的过程称为继承(inheritance),有关信息会在后续文章中介绍。

(二)对象

我们对象的三个主要特性:

  • 对象的行为(behavior)–可以对对象完成哪些操作,或者可以对对象使用哪些方法?
  • 对象的状态(state)–当调用那些方法时,对象会如何响应?
  • 对象的标识(identity)–如何区分具有相同行为与状态的不同对象?

同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法来定义的。

每个对象都保存着描述当前状态的信息(状态)。对象状态的改变必须通过调用方法来实现(封装性)。

对象的状态并不能完全描述一个对象。每个对象都有一个唯一的标识(identity)。

对象的这些关键特性会彼此相互影响。

(三)识别类

识别类的一个简单经验是在分析问题的过程中寻找名词,而方法对应着动词。

(四)类之间的关系

在类中,最常见的关系有:

  • 依赖(“uses-a“)
  • 聚合(”has-a“)
  • 继承(“is-a”)

依赖是一种最明显的、最常见的关系。如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。应该尽可能地将相互依赖地类减至最少。

聚合即包含关系。

继承表示一个更特殊的类与一个更一般的类之间的关系。

二、使用预定义类

(一)对象与对象变量

在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类

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运算符来调用。

以下是构造器的几个特性:

  • 构造器与类同名
  • 每个类可以有一个以上的构造器
  • 构造器可以有0个、1个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着new操作符一起调用

注:不要在构造器中定义与实例字段同名的局部变量。
同时可以使用var声明局部变量:

var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);

注意var关键字只能用于方法中的局部变量,参数和字段的类型必须声明。

(五)使用null引用

一个对象变量包含一个对象的引用,或者包含一个特殊值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%

并不使用构造器主要是由于:

  • 无法命名构造器。构造器的名字必须与类名相同。
  • 使用构造器时,无法改变构造对象的类型。而工厂方法实际上返回DecimalFormat类的对象,这是NumberFormat的一个子类。

(五)main方法

可以调用静态方法而不需要对象。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,则在类第一次加载时,会进行静态字段的初始化。

(八)对象析构与finalize方法

由于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文件可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。此外,JAR文件是压缩的,它使用了我们熟悉的ZIP压缩格式。

(一)创建JAR文件

可以使用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文件

可以使用jar命令中的e选项指定程序的入口点,即通常需要在调用java程序启动器时指定的类:

jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass files to add

或者,可以在清单文件中指定程序的主类,包括以下形式的语句:

Main-Class: com.mycompany.mypkg.MainAppClass

无论哪种方法,用户都可以通过以下命令启动程序:

java -jar MyProgram.jar

(四)多版本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:继承

你可能感兴趣的:(java,开发语言)