重新学习Java——对象和类(二)

上一节回归了如何以面向对象的思想去使用一些Java中的公共类,也设计了一些自己的类并介绍了设计类的基本方法和技巧,这一节我们将继续回顾这些内容,并争取从中获得新的体验和感受。

1. 静态域与静态方法

前面我们经常看到,main方法都被标记为static,我们现在就要讨论一下这个static的含义和内容。

1.1 静态域

如果将域定义为static,每个类中只有一个这样的域,而每个对象对于实例域来说都有一份自己的拷贝。比如下面这个例子:

class Employee {
  ...
  private int id;
  pirvate static int nextId = 1;
}

如果有1000个Employee对象,就有1000个实例域id。但只有一个nextId。即使没有Employee对象,nextId也存在。nextId属于类,id属于对象。下面有一个使用静态域的例子:

public void setId() {
  id = nextId;
  nextId++;
}

// 嘉定为harry设定雇员标志码
harry.setId();

// 这个功能类似于自动编码,每一个新的雇员的id会被设置为1,2,3...

1.2 静态常量

静态的变量在程序设计中使用的还是比较少的(上一节我本来不想用书本上的例子,但是想了半天也没想到更好的。。。),但静态常量的使用还是很多的。例如我们之前提到过的Math类,定义了静态常量:

在程序中,我们可以采用Math.PI,Math.E来获得这个形式的常量。如果没有static这个关键字,我们需要通过Math类的对象来访问PI,而且每一个Math都有一个PI的拷贝。

之前我们说过,每个类对象都可以修改公有域,所以最好不要将域设计为public。然而final却没有问题,final不允许覆盖为其他的值和对象。

在System类中,有一个setOut方法,可以将System.out设置为不同的流。虽然out设置为了final,但是却可以修改是为什么呢?原因是setOut是本地方法(native method),不是用Java编写的,因此可以绕过Java的存取控制机制。我们平时编写程序不应当这样处理。

1.3 静态方法

静态方法是一种不能像对象实时操作的方法。例如Math类的pow方法就是一个静态方法。运算时,不能够使用任何Math对象,所以没有隐式的参数。可以认为静态方法是没有this参数的方法。所以静态方法不能访问实例域,却可以访问自身类中的静态域。

可以使用对象调用静态方法,不过容易造成混淆,因为静态方法属于类而不是对象,如果通过类操作会让人产生混淆,和这个对象毫无关系。

在下面两种情况下使用静态方法:

  • 一个方法不需要访问对象状态,其所需参数都是通过显示参数提供
  • 一个方法只需要访问类的静态域

1.4 Factory方法

静态方法还有一种常见的用法,NumberFormat类采用factory方法产生不同风格的格式对象。

NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x));  // ¥0.10
System.out.println(percentFormatter.format(x));   // 10%

不采用不同的构造器来完成这些操作的原因主要有两个:

  • 无法为构造器命名
  • 使用构造器时,无法改变构造对象的类型

2. 方法参数

通常的成语语言在参数的传递时总是分为值传递引用传递。值传递表示方法接收的是调用者提供的值;引用传递结构的是调用者提供的变量地址。Java语言总是采用值传递,也就是说,方法得到的是所有参数值的一个拷贝,方法不能修改传递给它的任何参数变量的内容。

public static void main(String[] args) {
    int a = 1;
    System.out.println(a);   // 输出的是1
    tripleValue(a);
    System.out.println(a);   // 输出的还是1
}
    
public static void tripleValue(int x) {
    x = x * 3;
}

可以看到,调用这个方法之后,a的值仍然不变,具体的执行过程如下:

  1. x被初始化为a值的一个拷贝,也就是1
  2. x被乘以3后等于3,但a仍是1
  3. 方法执行结束,x消失

然而,方法参数总共有两种类型:基本数据类型;对象引用。因此,方法不能修改基本数据类型,而对象引用作为参数可以通过方法进行修改。

因此,很多人认为Java对对象采用的是引用传递,实际上这种理解是不对的。比如下面的这个程序:

public static void main(String[] args) {
    Employee x = new Employee("xxx", 10, 1, 1, 1);
    Employee y = new Employee("yyy", 10, 1, 1, 1);
    swap(x, y);
    System.out.println(x.getName());
    System.out.println(y.getName());
}

public static void swap(Employee x, Employee y) {
    Employee temp = x;
    x = y;
    x = temp;
}

结束之后,x的名字还是xxx,y的还是yyy。如果Java采用的是引用传递,他们的名字应该交换才对,所以有此可以看出,Java采用的是值传递

那么为什么我们传递一个对象之后,确实可以通过方法对对象进行修改呢?因为方法可以接收对象引用,将对象的引用作为一个值拷贝给了方法的变量x,x这是引用这个对象,因此可以对对象的实例域进行操作。而上述例子我们可以分析,swap方法的x拿到了x对象的引用,y同理,这时候,我们对x,y进行了交换,如今,x指向了y对象的引用,y指向了x的引用,但是本身Employee对象x,y的引用并没有变,因此不会交换他们自身的引用,导致出现上述结果。

因此我们总结一下在Java中,方法参数的使用情况:

  • 一个方法不能修改一个基本数据类型的参数
  • 一个方法可以改变一个对象参数的状态
  • 一个方法不能实现让对象参数引用一个新的对象

3. 对象构造

前面我们已经简单的看了构造器的编写,但由于构造器很重要,所以Java提供了多种编写方式。

3.1 重载

GregorianCalendar today = new GregorianCalendar ();
GregorianCalendar deadline = new GregorianCalendar(2099, 9, 31);

这种特性叫做重载(overloading)。如果多个方法,有相同的名字不同的参数,便产生了重载。编译器自动通过参数列表来匹配具体执行那个方法。如果找不到合适的参数或者多个匹配项,就会产生编译错误(重载解析)。Java允许重载任何方法,因此,要完整的描述一个方法,需要方法名参数类型,这叫做方法的签名。由于返回值不属于签名的一部分,也就是说,不能有两个名字相同、参数类型也相同,却返回不同类型的方法。

3.2 默认域初始化

如果在构造其中,没有显示地给域赋予初值,那么就会被自动的设置为默认值:数值为0、布尔为false、对象引用为null。然而,如果不显示的对域进行初始化,会影响程序的可读性。比如我们之前的Employee对象,如果没有初始化Date对象,则引用为null,这时调用hireDay可能会产生意想不到的错误。同时,域不仅可以通过常量赋值,还可以通过方法赋值。

3.3 默认构造器

如果在编写一个类的时候,没有编写构造器,那么系统会提供一个默认的构造器。这个默认的构造器将所有的实例域设置为默认值。但如果提供了自己编写的构造器,那么系统将不会提供默认的构造器。

3.4 调用另一个构造器

关键字this引用方法的隐式参数。如果构造器的第一个语句形如this(...),那么这个构造器要调用另一个构造器。

public Employee(double x) {
  this("Employee #" + nextId, x)
  nextId++;
}

当调用new Employee(60000)是,Employee(double)构造器将调用Employee(String, double)构造器。这样对公共构造器代码部分只用编写一次就行。

3.5 初始化块

初始化数据域的方法前面已经介绍了两种:在构造器中设置值,在声明中赋值。其实Java还可以通过初始化块来初始化数据域。

由于初始化数据域的途径多,所以可能出现混乱,下面列出了调用构造器的具体步骤:

  1. 所有数据域被初始化为默认值
  2. 按照在类生命中出现的次序,依次执行所有域初始化语句和初始化块
  3. 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体
  4. 执行这个构造器主体

如果对类的静态域进行初始化比较复杂,可以使用静态的初始化块

public class Hello {
  static {  // 这个static关键字表示这个类加载时调用,没有这个的话表示初始化一个类成员的时候调用
    System.out.println("Hello, World!");
  }
}

3.6 对象析构与finalize方法

Java有自动的垃圾回收器(GC),不需要人工回收内存,所以Java不支持析构器。当然,某些对象使用了系统之外的资源,如文件,数据库连接等,这时候当资源不再需要时,将其回首就很重要。

可以为任何类添加finalize方法,将在垃圾回收器清除对象之前调用。实际使用中最好不要使用这个方法,因为我们并不清楚这个方法的具体调用时间。

4. 包

Java允许通过包将类组织起来。通过包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。

使用包的主要原因是确保类名的唯一性。因此,Sun公司建议将公司的因特网域名以逆序的形式作为包名,并对不同的项目使用不同的子包。

4.1 类的导入

一个类可以使用所属包中的所有类,以及其他包中的公有类。我们可以采用两种方式访问另一个包中的公有类。

  1. 在每个类名之前添加完整的包名:java.util.Date today = new java.utill.Date();
  2. 通过import语句导入,import java.util.*; Date today = new Date();  还可以导入特定类:import java.util.Date;

通常我们应当将import语句写的尽可能详细,会使读者更加准确的知道加载了哪些类。而且使用*号只能导入一个包,而不能使用import java.*;导入多个以java为前缀的所有包。

有时候会发生命名冲突,比如java.util和java.sql包都有Date类,如果导入这两个包,会发生编译错误,编译器不知道使用哪个Date对象。这时可以采用增加特定import语句的方法来解决这个问题,如:java.util.Date;这样当编译器检测到Date对象时,会自动使用util包下的Date对象。或者当都需要使用的时候,在每个类之前加上完整的包名。

4.2 静态导入

从Jave SE 5.0开始,import不仅可以导入类,还增加了导入静态方法和静态域的功能。

例如:import static java.lang.System.*; 就可以使用System类的静态方法和静态域,而不必加类名:out.println();

实际上,这种编写不利于代码的清晰度,但是对于如Math类,使用起来却更加自然。

4.3  将类放入包中

要将一个类放入包中,就必须将包的名字放在源文件的开头。

package com.three

public class Test {
 ...
}

如果没有这个package语句,这个源文件就被放在一个默认包(default-package)中。包名和文件名是对应的,如com.three包中的文件在项目子目录com/three中。

4.4 包作用域

前面我们使用过public,private作为访问修饰符,如果都不加的话,就说明,可以被同一个包中的所有方法访问。

在默认的情况下,包不是一个封闭的尸体,也就是说,任何人都可以向包添加更多的类。在Java的早期版本中,通过package java.awit;可以轻松的将自己的类加入系统包中。借此访问java.awt中的资源了。从之后的版本起,JDK实现了类加载器,禁止了以“java.”开头的类。当然,用户自定义的类无法收益,但是可以通过包密封,解决问题。

类文件也可以存储在JAR(Java归档)文件中。在一个JAR中,可以包含多个压缩形式的类文件和子目录,既节约又可以改善性能。在程序中使用第三方的库文件时,通常会给出一个或多个JAR文件。

5. 文档注释

JDK有一个很有用的工具,叫做javadoc。它可以由源文件生成一个HTML文档。实际上,Java本身的API文档就是靠这个javadoc工具生成的。

在源代码中,通过以专用的定界符/**开始的注释,可以生成一个看上去很专业的文档。这种方式可以将代码与注释保存在一个地方。如果文档存入独立的文件,可能随着时间的推移,出现代码和注释不一致的问题。而是用了文档注释,只需要通过javadoc就可以很轻松的解决问题。

5.1 注释的插入

javadoc程序,从下面几个特性中抽取信息:

  • 公有类与接口
  • 公有的和受保护的方法
  • 公有的和受保护的域

应该为上面及部分编写注释。注释应该放置在所描述特性的前面。注释以/**开始,并以*/结束。每个/**..*/文档注释在标记之后紧跟着自由格式文本,标记由@开始,如@author。自由格式文本的第一句应该是概要性的句子。javadoc实用程序自动地将这些句子抽取出来形成概要页。在自由格式文本中,额可以使用HTML修饰符。但是不要使用<h1>或<hr>,会与文档格式产生冲突。

如果文档中用到其他文件的连接,就该将这些文件放到子目录doc-files中。例如<img src="doc-files/uml.png" alt="UML diagram" >。

5.2 类注释

类注释必须放在import豫剧之后,类定义之前。例如:

/**
 * The <code>System</code> class contains several useful class fields
 * and methods. It cannot be instantiated.
 *
 * <p>Among the facilities provided by the <code>System</code> class
 * are standard input, standard output, and error output streams;
 * access to externally defined properties and environment
 * variables; a means of loading files and libraries; and a utility
 * method for quickly copying a portion of an array.
 *
 * @author  unascribed
 * @since   JDK1.0
 */
public final class System {
  ......
}

并没有规定每一行必须用*开始,但是很多IDE提供了自动添加*的功能。

5.3 方法注释

每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用以下标记:

  • @param variable description 这个标记将对当前方法的参数部分添加一个条目。这个描述可以占据多行,并使用HTML标签。一个方法所有的@param必须放在一起
  • @return description 这个标记对当前方法添加返回部分,这个描述可以跨越多行,并可以使用HTML标记
  • @throws class description 这个标记添加一个注释,表示可能抛出的异常
  /**
     * Returns the unique {@link java.io.Console Console} object associated
     * with the current Java virtual machine, if any.
     *
     * @return  The system console, if any, otherwise <tt>null</tt>.
     *
     * @since   1.6
     */
     public static Console console() {
         if (cons == null) {
             synchronized (System.class) {
                 cons = sun.misc.SharedSecrets.getJavaIOAccess().console();
             }
         }
         return cons;
     }

5.4 域注释

只需要对公有域建立文档,如:

/* The security manager for the system.
 */
  private static volatile SecurityManager security = null;

5.5 通用注释

下面的标记可以用在类文档的注释中:

  • @author name 产生一个作者条目。可以使用多个@author标记,每个标记对应一个作者
  • @version text 产生一个版本条目,对应当前版本的任何描述

虾米那的标记可以用于所有的文档注释:

  • @since text 产生一个始于目录,这里的text可以是对引入特性的版本描述
  • @deprecagted text 对应类,方法或者变量添加一个不再使用的注释

5.6 包与概述注释

要想产生包注释,需要在每一个包的目录中添加一个单独的文件。有两个选择:

  1. 提供一个以package.html命名的HTML文件。在标记<body></body>之间的所有文本都会被抽取出来;
  2. 提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以/**和*/界定的Javadoc注释,跟随在一个包语句以后。不应该包含更多的代码或注释。

还可以为所有的源文件提供一个概述性的注释。这个注释将被放置在一个名为overview.html的文件中,这个文件位于包含所有源文件的父目录中。body之间的文本会被抽取出来。

6. 类设计技巧

《Java核心技术》这本书列出了一些简单的技巧,是的设计出来的类更具有OOP的专业水准。

  1. 一定要将数据设计为私有
  2. 一定要对数据初始化
  3. 不要在类中使用过多的基本数据类型
  4. 不是所有的域都需要独立的访问器和变更器
  5. 使用标准格式进行类的定义
  6. 将职责过多的类进行分解
  7. 类名和方法名要能够体现他们的指责

上述七条大部分还是很容易理解的,主要说一下3和5。

3:比如一个类,是People类,他有一些记录家庭地址的信息,如street,city,state等。这些数据,都是String型,我们应当做的是设计一个Address类,替换这些实例域。这样很容易对地址的变化进行处理,还可以增加国际化的处理。

5:书上提供了这样的一个顺序:

  • 公有访问特性部分
  • 包作用域访问特性部分
  • 私有访问特性部分

在每一部分中,应该按照下面的顺序列出:

  • 实例方法
  • 静态方法
  • 实例域
  • 静态域

作者认为,类的使用者对公有借口要比对私有接口实现的细节更感兴趣,并且对方法比数据更感兴趣。Sun公司建议先书写域,再书写方法。具体哪一种好并无定论,但是重要的是要保持一致性,使得别人接手的时候很容易的适应,当然,如果公司有要求,则是按公司的来。

posted @ 2013-09-28 16:28  大声大声道  阅读(410)  评论(0编辑  收藏  举报