• 重新学习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公司建议先书写域,再书写方法。具体哪一种好并无定论,但是重要的是要保持一致性,使得别人接手的时候很容易的适应,当然,如果公司有要求,则是按公司的来。

  • 相关阅读:
    婚礼珠宝策划
    Mandelbrot图像
    程序的又一次测量学实际应用(程序对全站仪测量学导出数据文件的读取与修改)
    中国海域系统源代码
    利用“三角化”计算行列式快速求解程序(验证过很多题目的,绝对准确)
    ”上三角“行列式源代码(改良版,添加了几种特殊情况的特殊处理)
    中国海域系统
    Java
    Java
    【日记】12.12
  • 原文地址:https://www.cnblogs.com/yijianguxin/p/3342446.html
Copyright © 2020-2023  润新知